payment-kit 1.13.26 → 1.13.27
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/index.ts +8 -1
- package/api/src/integrations/blockchain/nft.ts +125 -0
- package/api/src/integrations/blockchain/stake.ts +55 -0
- package/api/src/integrations/blocklet/notification.ts +101 -0
- package/api/src/integrations/blocklet/passport.ts +139 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +1 -1
- package/api/src/integrations/stripe/resource.ts +7 -7
- package/api/src/integrations/stripe/setup.ts +1 -1
- package/api/src/jobs/checkout-session.ts +23 -0
- package/api/src/jobs/payment.ts +1 -2
- package/api/src/libs/audit.ts +44 -2
- package/api/src/libs/payment.ts +3 -4
- package/api/src/locales/en.ts +9 -1
- package/api/src/locales/zh.ts +9 -1
- package/api/src/routes/checkout-sessions.ts +44 -14
- package/api/src/routes/connect/collect.ts +1 -2
- package/api/src/routes/connect/pay.ts +1 -2
- package/api/src/routes/connect/setup.ts +1 -2
- package/api/src/routes/connect/shared.ts +7 -3
- package/api/src/routes/connect/subscribe.ts +2 -3
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/integrations/stripe.ts +1 -1
- package/api/src/routes/passports.ts +74 -0
- package/api/src/routes/payment-links.ts +12 -2
- package/api/src/routes/pricing-table.ts +17 -3
- package/api/src/routes/products.ts +3 -3
- package/api/src/routes/redirect.ts +18 -0
- package/api/src/routes/subscriptions.ts +2 -5
- package/api/src/store/migrations/20231021-nft.ts +22 -0
- package/api/src/store/models/checkout-session.ts +76 -20
- package/api/src/store/models/invoice.ts +2 -0
- package/api/src/store/models/payment-intent.ts +2 -0
- package/api/src/store/models/payment-link.ts +26 -15
- package/api/src/store/models/payment-method.ts +22 -1
- package/api/src/store/models/price.ts +2 -0
- package/api/src/store/models/subscription.ts +26 -4
- package/api/src/store/models/types.ts +32 -1
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +7 -5
- package/src/components/customer/actions.tsx +15 -17
- package/src/components/customer/form.tsx +1 -1
- package/src/components/invoice/list.tsx +2 -1
- package/src/components/passport/actions.tsx +62 -0
- package/src/components/passport/assign.tsx +82 -0
- package/src/components/payment-intent/list.tsx +5 -1
- package/src/components/payment-link/actions.tsx +14 -1
- package/src/components/payment-link/after-pay.tsx +33 -1
- package/src/components/payment-link/preview.tsx +3 -6
- package/src/components/price/form.tsx +22 -23
- package/src/components/pricing-table/actions.tsx +14 -1
- package/src/components/pricing-table/payment-settings.tsx +33 -1
- package/src/components/pricing-table/preview.tsx +3 -7
- package/src/components/pricing-table/product-settings.tsx +4 -0
- package/src/components/pricing-table/product-skeleton.tsx +39 -0
- package/src/components/product/actions.tsx +14 -1
- package/src/components/status.tsx +1 -1
- package/src/components/subscription/status.tsx +3 -3
- package/src/components/table.tsx +14 -4
- package/src/global.css +7 -5
- package/src/libs/util.ts +6 -0
- package/src/locales/en.tsx +53 -2
- package/src/locales/zh.tsx +272 -116
- package/src/pages/admin/payments/links/create.tsx +4 -0
- package/src/pages/admin/payments/links/detail.tsx +9 -4
- package/src/pages/admin/products/index.tsx +2 -0
- package/src/pages/admin/products/passports/index.tsx +154 -0
- package/src/pages/admin/products/pricing-tables/create.tsx +1 -1
- package/src/pages/admin/settings/index.tsx +1 -1
- package/src/pages/admin/settings/payment-methods/index.tsx +17 -7
- package/src/pages/checkout/pay.tsx +15 -13
- package/src/pages/checkout/pricing-table.tsx +127 -91
- package/api/src/libs/chain/arcblock.ts +0 -13
package/api/src/index.ts
CHANGED
|
@@ -9,7 +9,9 @@ import dotenv from 'dotenv-flow';
|
|
|
9
9
|
import express, { ErrorRequestHandler, Request, Response } from 'express';
|
|
10
10
|
import morgan from 'morgan';
|
|
11
11
|
|
|
12
|
+
import { ensureStakedForGas } from './integrations/blockchain/stake';
|
|
12
13
|
import { ensureWebhookRegistered } from './integrations/stripe/setup';
|
|
14
|
+
import { startCheckoutSessionQueue } from './jobs/checkout-session';
|
|
13
15
|
import { startEventQueue } from './jobs/event';
|
|
14
16
|
import { startInvoiceQueue } from './jobs/invoice';
|
|
15
17
|
import { startPaymentQueue } from './jobs/payment';
|
|
@@ -98,6 +100,11 @@ export const server = app.listen(port, (err?: any) => {
|
|
|
98
100
|
startInvoiceQueue().then(() => logger.info('invoice queue started'));
|
|
99
101
|
startSubscriptionQueue().then(() => logger.info('subscription queue started'));
|
|
100
102
|
startEventQueue().then(() => logger.info('event queue started'));
|
|
103
|
+
startCheckoutSessionQueue().then(() => logger.info('checkoutSession queue started'));
|
|
101
104
|
|
|
102
|
-
|
|
105
|
+
if (process.env.BLOCKLET_MODE === 'production') {
|
|
106
|
+
ensureWebhookRegistered().catch(console.error);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
ensureStakedForGas().catch(console.error);
|
|
103
110
|
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// import pick from 'lodash/pick';
|
|
2
|
+
import { formatFactoryState, preMintFromFactory } from '@ocap/asset';
|
|
3
|
+
import merge from 'lodash/merge';
|
|
4
|
+
|
|
5
|
+
import { wallet } from '../../libs/auth';
|
|
6
|
+
import logger from '../../libs/logger';
|
|
7
|
+
import { CheckoutSession, PaymentCurrency, PaymentIntent, PaymentMethod, Subscription } from '../../store/models';
|
|
8
|
+
import { sendNftNotification } from '../blocklet/notification';
|
|
9
|
+
|
|
10
|
+
export async function mintNftForCheckoutSession(id: string) {
|
|
11
|
+
const checkoutSession = await CheckoutSession.findByPk(id);
|
|
12
|
+
if (!checkoutSession) {
|
|
13
|
+
logger.warn('checkoutSession not found when attempt to mint nft', { id });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (checkoutSession.status !== 'complete') {
|
|
18
|
+
logger.warn('checkoutSession not completed when attempt to mint nft', { id });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (checkoutSession.nft_mint_status === 'disabled') {
|
|
23
|
+
logger.warn('nft mint not enabled for checkoutSession', { id, settings: checkoutSession.nft_mint_settings });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (checkoutSession.nft_mint_status === 'minted') {
|
|
28
|
+
logger.warn('checkoutSession nft already minted', { id, details: checkoutSession.nft_mint_details });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (checkoutSession.nft_mint_status === 'error') {
|
|
33
|
+
logger.warn('checkoutSession nft already error', { id, details: checkoutSession.nft_mint_details });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// TODO: we may need retry here when the chain is temporarily inaccessible
|
|
38
|
+
const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
|
|
39
|
+
const method = await PaymentMethod.findByPk(currency?.payment_method_id);
|
|
40
|
+
if (method?.type === 'arcblock') {
|
|
41
|
+
const client = method.getOcapClient();
|
|
42
|
+
const nftOwner = checkoutSession.customer_did;
|
|
43
|
+
const { factory, inputs } = checkoutSession.nft_mint_settings as any;
|
|
44
|
+
|
|
45
|
+
const [{ state: factoryState }, { state: appState }] = await Promise.all([
|
|
46
|
+
client.getFactoryState({ address: factory }),
|
|
47
|
+
client.getAccountState({ address: wallet.address }),
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const preMint = preMintFromFactory({
|
|
51
|
+
factory: formatFactoryState(factoryState),
|
|
52
|
+
inputs: inputs || {},
|
|
53
|
+
owner: nftOwner,
|
|
54
|
+
issuer: { wallet, name: appState.moniker },
|
|
55
|
+
});
|
|
56
|
+
logger.info('nft preMint for checkoutSession', { id, inputs, nftOwner, factory, preMint });
|
|
57
|
+
|
|
58
|
+
const { state: nftState } = await client.getAccountState({ address: preMint.address });
|
|
59
|
+
if (nftState) {
|
|
60
|
+
await checkoutSession.update({
|
|
61
|
+
nft_mint_status: 'error',
|
|
62
|
+
nft_mint_details: {
|
|
63
|
+
arcblock: {
|
|
64
|
+
address: preMint.address,
|
|
65
|
+
owner: nftOwner as string,
|
|
66
|
+
error: 'NFT with same address already exist',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
logger.warn('nft mint duplicate for checkoutSession', { id, details: checkoutSession.nft_mint_details });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const hash = await client.sendMintAssetTx({
|
|
75
|
+
tx: {
|
|
76
|
+
itx: {
|
|
77
|
+
factory,
|
|
78
|
+
address: preMint.address,
|
|
79
|
+
assets: [],
|
|
80
|
+
variables: Object.entries(preMint.variables).map(([key, value]) => ({ name: key, value })),
|
|
81
|
+
owner: nftOwner,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
wallet,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
logger.info('nft minted for checkoutSession', { id, inputs, nftOwner, factory, hash });
|
|
88
|
+
await checkoutSession.update({
|
|
89
|
+
nft_mint_status: 'minted',
|
|
90
|
+
nft_mint_details: {
|
|
91
|
+
arcblock: {
|
|
92
|
+
tx_hash: hash,
|
|
93
|
+
address: preMint.address,
|
|
94
|
+
owner: nftOwner as string,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (checkoutSession.subscription_id) {
|
|
100
|
+
const subscription = await Subscription.findByPk(checkoutSession.subscription_id);
|
|
101
|
+
if (subscription && subscription.metadata?.nft_address) {
|
|
102
|
+
await subscription.update({ metadata: merge(subscription.metadata || {}, { nft_address: preMint.address }) });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (checkoutSession.payment_intent_id) {
|
|
106
|
+
const paymentIntent = await PaymentIntent.findByPk(checkoutSession.payment_intent_id);
|
|
107
|
+
if (paymentIntent && paymentIntent.metadata?.nft_address) {
|
|
108
|
+
await paymentIntent.update({ metadata: merge(paymentIntent.metadata || {}, { nft_address: preMint.address }) });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await sendNftNotification(
|
|
113
|
+
preMint.address,
|
|
114
|
+
nftOwner as string,
|
|
115
|
+
method.settings.arcblock?.api_host as string,
|
|
116
|
+
factoryState.name
|
|
117
|
+
);
|
|
118
|
+
logger.info('nft sent for checkoutSession', { id, nftOwner });
|
|
119
|
+
await checkoutSession.update({ nft_mint_status: 'sent' });
|
|
120
|
+
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
logger.warn('nft mint not supported for payment method', { id, type: method?.type });
|
|
125
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* eslint-disable no-continue */
|
|
2
|
+
/* eslint-disable no-await-in-loop */
|
|
3
|
+
import { toStakeAddress } from '@arcblock/did-util';
|
|
4
|
+
import env from '@blocklet/sdk/lib/env';
|
|
5
|
+
import { fromUnitToToken, toBN } from '@ocap/util';
|
|
6
|
+
|
|
7
|
+
import { wallet } from '../../libs/auth';
|
|
8
|
+
import logger from '../../libs/logger';
|
|
9
|
+
import { PaymentCurrency, PaymentMethod } from '../../store/models';
|
|
10
|
+
|
|
11
|
+
export async function ensureStakedForGas() {
|
|
12
|
+
const currencies = await PaymentCurrency.findAll({ where: { active: true, is_base_currency: true } });
|
|
13
|
+
|
|
14
|
+
for (const currency of currencies) {
|
|
15
|
+
const method = await PaymentMethod.findByPk(currency.payment_method_id);
|
|
16
|
+
if (method && method.type === 'arcblock') {
|
|
17
|
+
const client = method.getOcapClient();
|
|
18
|
+
const host = method.settings.arcblock?.api_host;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const { state: account } = await client.getAccountState({ address: wallet.address });
|
|
22
|
+
if (!account) {
|
|
23
|
+
const hash = await client.declare({ moniker: env.appNameSlug, wallet });
|
|
24
|
+
logger.info(`declared app on chain ${host}`, { hash });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const address = toStakeAddress(wallet.address, wallet.address);
|
|
28
|
+
const { state: stake } = await client.getStakeState({ address });
|
|
29
|
+
if (stake) {
|
|
30
|
+
logger.info(`app already staked for gas on chain ${host}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = await client.getForgeState({});
|
|
35
|
+
const { token, txConfig } = result.state;
|
|
36
|
+
const holding = account.tokens.find((x: any) => x.address === token.address);
|
|
37
|
+
if (toBN(holding?.value || '0').lte(toBN(txConfig.txGas.minStake))) {
|
|
38
|
+
logger.info(`not enough balance to stake for gas on chain ${host}`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
const [hash] = await client.stake({
|
|
44
|
+
to: wallet.address,
|
|
45
|
+
message: 'stake-for-gas',
|
|
46
|
+
tokens: [{ address: token.address, value: fromUnitToToken(txConfig.txGas.minStake, token.decimal) }],
|
|
47
|
+
wallet,
|
|
48
|
+
});
|
|
49
|
+
logger.info(`staked for gas on chain ${host}`, { hash });
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.error(`stake for gas on chain ${host} failed`, err);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import Notification from '@blocklet/sdk/service/notification';
|
|
2
|
+
import { toDid } from '@ocap/util';
|
|
3
|
+
|
|
4
|
+
import { blocklet } from '../../libs/auth';
|
|
5
|
+
import logger from '../../libs/logger';
|
|
6
|
+
import { translate } from '../../locales';
|
|
7
|
+
|
|
8
|
+
export async function getUserLocale(userDid: string) {
|
|
9
|
+
const { user } = await blocklet.getUser(userDid);
|
|
10
|
+
return user?.locale || 'en';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createInfoRows(info: any = {}) {
|
|
14
|
+
const fields = Object.keys(info).reduce((list, cur) => {
|
|
15
|
+
const item = info[cur];
|
|
16
|
+
const data = {
|
|
17
|
+
type: 'plain',
|
|
18
|
+
text: item,
|
|
19
|
+
color: undefined,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (Array.isArray(item)) {
|
|
23
|
+
const [text, color] = item;
|
|
24
|
+
data.text = text;
|
|
25
|
+
if (color) {
|
|
26
|
+
data.color = color;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (typeof data.text !== 'string') {
|
|
31
|
+
data.text = String(data.text);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
list.push(
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
{
|
|
37
|
+
type: 'text',
|
|
38
|
+
data: {
|
|
39
|
+
type: 'plain',
|
|
40
|
+
color: '#9397A1',
|
|
41
|
+
text: cur,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: 'text',
|
|
46
|
+
data,
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
return list;
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
type: 'section',
|
|
54
|
+
fields,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function sendNotification({
|
|
59
|
+
to,
|
|
60
|
+
title,
|
|
61
|
+
message,
|
|
62
|
+
actions,
|
|
63
|
+
attachments = [],
|
|
64
|
+
}: {
|
|
65
|
+
to: string;
|
|
66
|
+
title: string;
|
|
67
|
+
message: string;
|
|
68
|
+
actions: any[];
|
|
69
|
+
attachments: any[];
|
|
70
|
+
}) {
|
|
71
|
+
try {
|
|
72
|
+
const payload = { title, body: message, actions: actions || [], attachments: [...attachments] };
|
|
73
|
+
await Notification.sendToUser(to, payload);
|
|
74
|
+
logger.info('text message was sent', { to, payload: JSON.stringify(payload, null, 2) });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
logger.error('send text message failed', { error, to, message, actions, attachments });
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function sendNftNotification(nftDid: string, userDid: string, chainHost: string, collection: string) {
|
|
82
|
+
const locale = await getUserLocale(userDid);
|
|
83
|
+
await sendNotification({
|
|
84
|
+
to: userDid,
|
|
85
|
+
title: translate('notification.mintNFT.title', locale, { collection }),
|
|
86
|
+
message: translate('notification.mintNFT.message', locale, { collection }),
|
|
87
|
+
attachments: [
|
|
88
|
+
{
|
|
89
|
+
type: 'asset',
|
|
90
|
+
data: {
|
|
91
|
+
chainHost,
|
|
92
|
+
did: nftDid,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
createInfoRows({
|
|
96
|
+
[translate('notification.sendTo', locale)]: toDid(userDid),
|
|
97
|
+
}),
|
|
98
|
+
],
|
|
99
|
+
actions: [],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import merge from 'lodash/merge';
|
|
2
|
+
import pick from 'lodash/pick';
|
|
3
|
+
|
|
4
|
+
import { blocklet } from '../../libs/auth';
|
|
5
|
+
import logger from '../../libs/logger';
|
|
6
|
+
import {
|
|
7
|
+
CheckoutSession,
|
|
8
|
+
PaymentLink,
|
|
9
|
+
Price,
|
|
10
|
+
PricingTable,
|
|
11
|
+
Product,
|
|
12
|
+
Subscription,
|
|
13
|
+
TLineItemExpanded,
|
|
14
|
+
} from '../../store/models';
|
|
15
|
+
|
|
16
|
+
export async function checkPassportForPaymentLink(doc: PaymentLink) {
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
const items: TLineItemExpanded[] = await Price.expand(doc.line_items, true);
|
|
19
|
+
const item = items.find((x) => x.price.product?.metadata?.passport);
|
|
20
|
+
return item?.price.product?.metadata?.passport;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function checkPassportForPricingTable(doc: PricingTable) {
|
|
24
|
+
const products = await Product.findAll({ where: { id: doc?.items.map((x) => x.product_id) } });
|
|
25
|
+
const product = products.find((x) => x.metadata?.passport);
|
|
26
|
+
return product?.metadata?.passport;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function ensurePassportIssued(checkoutSession: CheckoutSession) {
|
|
30
|
+
if (checkoutSession.status !== 'complete') {
|
|
31
|
+
logger.warn('checkoutSession not complete', pick(checkoutSession, ['id', 'status']));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const info = pick(checkoutSession, ['id', 'metadata']);
|
|
35
|
+
if (!checkoutSession.customer_did) {
|
|
36
|
+
logger.warn('checkoutSession have no customer did', info);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!checkoutSession.metadata?.passport) {
|
|
40
|
+
logger.warn('checkoutSession not contain any passport issue info', info);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
45
|
+
const { passport, pricing_table_id } = checkoutSession.metadata as any;
|
|
46
|
+
const { role } = await blocklet.getRole(passport);
|
|
47
|
+
if (!role) {
|
|
48
|
+
logger.warn('role not found for passport issuance', info);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!role.extra?.acquire?.pay || !role.extra?.payment?.product) {
|
|
53
|
+
logger.warn('role config not valid for passport issuance', { ...info, role });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const productId = role.extra.payment.product;
|
|
58
|
+
|
|
59
|
+
// pre issuance check
|
|
60
|
+
if (pricing_table_id) {
|
|
61
|
+
if (role.extra?.acquire?.pay !== pricing_table_id) {
|
|
62
|
+
logger.warn('source pricing table not match wth role config', merge(info, { role }));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const doc = await PricingTable.findByPk(pricing_table_id);
|
|
67
|
+
if (!doc) {
|
|
68
|
+
logger.warn('source pricing table not found for passport issuance', info);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (doc.items.some((x) => x.product_id === productId) === false) {
|
|
73
|
+
logger.warn('source pricing table not contain product for passport issuance', { ...info, productId });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
} else if (checkoutSession.payment_link_id) {
|
|
77
|
+
if (role.extra?.acquire?.pay !== checkoutSession.payment_link_id) {
|
|
78
|
+
logger.warn('source payment link not match wth role config', merge(info, { role }));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const doc = await PaymentLink.findByPk(checkoutSession.payment_link_id);
|
|
83
|
+
if (!doc) {
|
|
84
|
+
logger.warn('source payment link not found for passport issuance', info);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const items = await Price.expand(doc.line_items);
|
|
89
|
+
if (items.some((x) => x.price.product_id === productId) === false) {
|
|
90
|
+
logger.warn('source payment link not contain product for passport issuance', { ...info, productId });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
logger.warn('no payment link or pricing table for passport issuance', info);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// do the issuance
|
|
98
|
+
await blocklet.issuePassportToUser({ userDid: checkoutSession.customer_did, role: passport });
|
|
99
|
+
logger.info(
|
|
100
|
+
'passport issued to user on checkout session complete',
|
|
101
|
+
merge(info, { role, customer: checkoutSession.customer_did })
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function ensurePassportRevoked(subscription: Subscription) {
|
|
106
|
+
const info = pick(subscription, ['id', 'status']);
|
|
107
|
+
if (subscription.status !== 'canceled') {
|
|
108
|
+
logger.warn('subscription not canceled', info);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const checkoutSession = await CheckoutSession.findOne({ where: { subscription_id: subscription.id } });
|
|
113
|
+
if (!checkoutSession) {
|
|
114
|
+
logger.warn('checkoutSession for subscription not found', info);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!checkoutSession.metadata?.passport) {
|
|
119
|
+
logger.warn('checkoutSession not contain any passport issue info', info);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const { user } = await blocklet.getUser(checkoutSession.customer_did as string, { enableConnectedAccount: true });
|
|
124
|
+
if (!user) {
|
|
125
|
+
logger.warn(
|
|
126
|
+
'user not found when revoke on subscription cancel',
|
|
127
|
+
merge(info, { did: checkoutSession.customer_did })
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { passport } = checkoutSession.metadata as any;
|
|
133
|
+
const passports = user.passports.filter((x: any) => x.role === passport && x.status === 'valid');
|
|
134
|
+
await Promise.all(passports.map((x: any) => blocklet.revokeUserPassport({ userDid: user.did, passportId: x.id })));
|
|
135
|
+
logger.warn(
|
|
136
|
+
'user passports revoked on subscription cancel',
|
|
137
|
+
merge(info, { did: checkoutSession.customer_did, passports })
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -211,7 +211,7 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
|
|
|
211
211
|
if (subscription) {
|
|
212
212
|
const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
213
213
|
if (method && method.type === 'stripe') {
|
|
214
|
-
const tmp = await ensureStripeInvoice(event.data.object, subscription, method.
|
|
214
|
+
const tmp = await ensureStripeInvoice(event.data.object, subscription, method.getStripeClient());
|
|
215
215
|
localInvoiceId = tmp.id;
|
|
216
216
|
}
|
|
217
217
|
}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from '../../store/models';
|
|
17
17
|
|
|
18
18
|
export async function ensureStripeProduct(internal: Product, method: PaymentMethod) {
|
|
19
|
-
const client = method.
|
|
19
|
+
const client = method.getStripeClient();
|
|
20
20
|
const result = await client.products.search({ query: `metadata['id']:'${internal.id}'` });
|
|
21
21
|
if (result.data.length > 0) {
|
|
22
22
|
return result.data[0] as any;
|
|
@@ -45,7 +45,7 @@ export async function ensureStripeProduct(internal: Product, method: PaymentMeth
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export async function ensureStripePrice(internal: Price, method: PaymentMethod, currency: PaymentCurrency) {
|
|
48
|
-
const client = method.
|
|
48
|
+
const client = method.getStripeClient();
|
|
49
49
|
const result = await client.prices.search({ query: `metadata['id']:'${internal.id}'` });
|
|
50
50
|
if (result.data.length > 0) {
|
|
51
51
|
return result.data[0] as any;
|
|
@@ -107,7 +107,7 @@ export async function ensureStripePrice(internal: Price, method: PaymentMethod,
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
export async function ensureStripeCustomer(internal: Customer, method: PaymentMethod) {
|
|
110
|
-
const client = method.
|
|
110
|
+
const client = method.getStripeClient();
|
|
111
111
|
const result = await client.customers.search({ query: `metadata['did']:'${internal.did}'` });
|
|
112
112
|
if (result.data.length > 0) {
|
|
113
113
|
return result.data[0] as any;
|
|
@@ -132,7 +132,7 @@ export async function ensureStripeCustomer(internal: Customer, method: PaymentMe
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
export async function ensureStripePaymentCustomer(internal: any, method: PaymentMethod) {
|
|
135
|
-
const client = method.
|
|
135
|
+
const client = method.getStripeClient();
|
|
136
136
|
let customer = null;
|
|
137
137
|
if (internal.payment_details?.stripe?.customer_id) {
|
|
138
138
|
customer = await client.customers.retrieve(internal.payment_details.stripe.customer_id);
|
|
@@ -149,7 +149,7 @@ export async function ensureStripePaymentIntent(
|
|
|
149
149
|
method: PaymentMethod,
|
|
150
150
|
currency: PaymentCurrency
|
|
151
151
|
) {
|
|
152
|
-
const client = method.
|
|
152
|
+
const client = method.getStripeClient();
|
|
153
153
|
|
|
154
154
|
let stripeIntent = null;
|
|
155
155
|
if (internal.payment_details?.stripe?.payment_intent_id) {
|
|
@@ -193,7 +193,7 @@ export async function ensureStripeSubscription(
|
|
|
193
193
|
items: TLineItemExpanded[],
|
|
194
194
|
trialInDays: number = 0
|
|
195
195
|
) {
|
|
196
|
-
const client = method.
|
|
196
|
+
const client = method.getStripeClient();
|
|
197
197
|
|
|
198
198
|
let stripeSubscription: any = null;
|
|
199
199
|
if (internal.payment_details?.stripe?.subscription_id) {
|
|
@@ -303,7 +303,7 @@ export async function forwardUsageRecordToStripe(
|
|
|
303
303
|
return null;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
const client = method.
|
|
306
|
+
const client = method.getStripeClient();
|
|
307
307
|
const result = await client.subscriptionItems.createUsageRecord(subscriptionItem.metadata.stripe_id, {
|
|
308
308
|
quantity: updates.quantity,
|
|
309
309
|
timestamp: updates.timestamp,
|
|
@@ -16,7 +16,7 @@ export async function ensureWebhookRegistered() {
|
|
|
16
16
|
continue;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const stripe = method.
|
|
19
|
+
const stripe = method.getStripeClient();
|
|
20
20
|
const { data } = await stripe.webhookEndpoints.list({ limit: 100 });
|
|
21
21
|
|
|
22
22
|
const exist = data.find((webhook) => webhook.metadata?.appPid === env.appPid);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { mintNftForCheckoutSession } from '../integrations/blockchain/nft';
|
|
2
|
+
import { ensurePassportIssued, ensurePassportRevoked } from '../integrations/blocklet/passport';
|
|
3
|
+
import { events } from '../libs/event';
|
|
4
|
+
import logger from '../libs/logger';
|
|
5
|
+
import type { CheckoutSession, Subscription } from '../store/models';
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line require-await
|
|
8
|
+
export async function startCheckoutSessionQueue() {
|
|
9
|
+
events.on('checkout.session.completed', (checkoutSession: CheckoutSession) => {
|
|
10
|
+
ensurePassportIssued(checkoutSession).catch((err) => {
|
|
11
|
+
logger.error('ensurePassportIssued failed', { error: err, checkoutSession: checkoutSession.id });
|
|
12
|
+
});
|
|
13
|
+
mintNftForCheckoutSession(checkoutSession.id).catch((err) => {
|
|
14
|
+
logger.error('mintNftForCheckoutSession failed', { error: err, checkoutSession: checkoutSession.id });
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
events.on('customer.subscription.deleted', (subscription: Subscription) => {
|
|
19
|
+
ensurePassportRevoked(subscription).catch((err) => {
|
|
20
|
+
logger.error('ensurePassportRevoked failed', { error: err, subscription: subscription.id });
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
package/api/src/jobs/payment.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { wallet } from '../libs/auth';
|
|
2
|
-
import { getClient } from '../libs/chain/arcblock';
|
|
3
2
|
import dayjs from '../libs/dayjs';
|
|
4
3
|
import logger from '../libs/logger';
|
|
5
4
|
import createQueue from '../libs/queue';
|
|
@@ -61,7 +60,7 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
61
60
|
logger.info(`PaymentIntent capture attempt: ${paymentIntent.id}`);
|
|
62
61
|
try {
|
|
63
62
|
await paymentIntent.update({ status: 'processing' });
|
|
64
|
-
const client =
|
|
63
|
+
const client = paymentMethod.getOcapClient();
|
|
65
64
|
const txHash = await client.sendTransferV2Tx({
|
|
66
65
|
tx: {
|
|
67
66
|
itx: {
|
package/api/src/libs/audit.ts
CHANGED
|
@@ -3,6 +3,8 @@ import pick from 'lodash/pick';
|
|
|
3
3
|
import { Event } from '../store/models/event';
|
|
4
4
|
import { events } from './event';
|
|
5
5
|
|
|
6
|
+
const API_VERSION = '2023-09-05';
|
|
7
|
+
|
|
6
8
|
export async function createEvent(scope: string, type: string, model: any, options: any) {
|
|
7
9
|
// console.log('createEvent', scope, type, model, options);
|
|
8
10
|
const data: any = {
|
|
@@ -14,7 +16,7 @@ export async function createEvent(scope: string, type: string, model: any, optio
|
|
|
14
16
|
|
|
15
17
|
const event = await Event.create({
|
|
16
18
|
type,
|
|
17
|
-
api_version:
|
|
19
|
+
api_version: API_VERSION,
|
|
18
20
|
livemode: !!model.livemode,
|
|
19
21
|
object_id: model.id,
|
|
20
22
|
object_type: scope,
|
|
@@ -29,6 +31,7 @@ export async function createEvent(scope: string, type: string, model: any, optio
|
|
|
29
31
|
});
|
|
30
32
|
|
|
31
33
|
events.emit('event.created', { id: event.id });
|
|
34
|
+
events.emit(event.type, data.object);
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export async function createStatusEvent(
|
|
@@ -55,7 +58,45 @@ export async function createStatusEvent(
|
|
|
55
58
|
const suffix = config[data.object.status];
|
|
56
59
|
const event = await Event.create({
|
|
57
60
|
type: [prefix, suffix].join('.'),
|
|
58
|
-
api_version:
|
|
61
|
+
api_version: API_VERSION,
|
|
62
|
+
livemode: !!model.livemode,
|
|
63
|
+
object_id: model.id,
|
|
64
|
+
object_type: scope,
|
|
65
|
+
data,
|
|
66
|
+
request: {
|
|
67
|
+
// FIXME:
|
|
68
|
+
id: '',
|
|
69
|
+
idempotency_key: '',
|
|
70
|
+
},
|
|
71
|
+
metadata: {},
|
|
72
|
+
pending_webhooks: 99, // force all events goto the event queue
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
events.emit('event.created', { id: event.id });
|
|
76
|
+
events.emit(event.type, data.object);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function createCustomEvent(
|
|
80
|
+
scope: string,
|
|
81
|
+
prefix: string,
|
|
82
|
+
type: (current: any, previous: any) => string,
|
|
83
|
+
model: any,
|
|
84
|
+
options: any
|
|
85
|
+
) {
|
|
86
|
+
// console.log('createCustomEvent', scope, prefix, type, model, options);
|
|
87
|
+
const data: any = {
|
|
88
|
+
object: model.dataValues,
|
|
89
|
+
previous_attributes: pick(model._previousDataValues, options.fields),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const suffix = type(data.object, data.previous_attributes);
|
|
93
|
+
if (!suffix) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const event = await Event.create({
|
|
98
|
+
type: [prefix, suffix].join('.'),
|
|
99
|
+
api_version: API_VERSION,
|
|
59
100
|
livemode: !!model.livemode,
|
|
60
101
|
object_id: model.id,
|
|
61
102
|
object_type: scope,
|
|
@@ -70,4 +111,5 @@ export async function createStatusEvent(
|
|
|
70
111
|
});
|
|
71
112
|
|
|
72
113
|
events.emit('event.created', { id: event.id });
|
|
114
|
+
events.emit(event.type, data.object);
|
|
73
115
|
}
|
package/api/src/libs/payment.ts
CHANGED
|
@@ -4,12 +4,11 @@ import type { DelegateState } from '@ocap/client';
|
|
|
4
4
|
import { BN } from '@ocap/util';
|
|
5
5
|
|
|
6
6
|
import type { TPaymentCurrency } from '../store/models/payment-currency';
|
|
7
|
-
import type {
|
|
7
|
+
import type { PaymentMethod } from '../store/models/payment-method';
|
|
8
8
|
import { blocklet, wallet } from './auth';
|
|
9
|
-
import { getClient } from './chain/arcblock';
|
|
10
9
|
|
|
11
10
|
export async function isDelegationSufficientForPayment(args: {
|
|
12
|
-
paymentMethod:
|
|
11
|
+
paymentMethod: PaymentMethod;
|
|
13
12
|
paymentCurrency: TPaymentCurrency;
|
|
14
13
|
userDid: string;
|
|
15
14
|
amount: string;
|
|
@@ -23,7 +22,7 @@ export async function isDelegationSufficientForPayment(args: {
|
|
|
23
22
|
return { sufficient: false, reason: 'NO_DID_WALLET' };
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
const client =
|
|
25
|
+
const client = paymentMethod.getOcapClient();
|
|
27
26
|
|
|
28
27
|
// have delegated before?
|
|
29
28
|
const address = toDelegateAddress(delegator, wallet.address);
|
package/api/src/locales/en.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import flat from 'flat';
|
|
2
2
|
|
|
3
|
-
export default flat({
|
|
3
|
+
export default flat({
|
|
4
|
+
notification: {
|
|
5
|
+
sendTo: 'Sent to',
|
|
6
|
+
mintNFT: {
|
|
7
|
+
title: '{collection} NFT minted',
|
|
8
|
+
message: 'A new {collection} NFT is minted and sent to your wallet, please check it out.',
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
});
|