payment-kit 1.13.17 → 1.13.19
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/README.md +14 -0
- package/api/src/index.ts +17 -6
- package/api/src/integrations/stripe/handlers/index.ts +53 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +252 -0
- package/api/src/integrations/stripe/handlers/payment-intent.ts +172 -0
- package/api/src/integrations/stripe/handlers/setup-intent.ts +42 -0
- package/api/src/integrations/stripe/handlers/subscription.ts +61 -0
- package/api/src/integrations/stripe/resource.ts +317 -0
- package/api/src/integrations/stripe/setup.ts +50 -0
- package/api/src/jobs/invoice.ts +11 -0
- package/api/src/jobs/payment.ts +15 -7
- package/api/src/jobs/subscription.ts +18 -2
- package/api/src/libs/session.ts +104 -8
- package/api/src/libs/util.ts +47 -1
- package/api/src/routes/checkout-sessions.ts +134 -27
- package/api/src/routes/connect/collect.ts +12 -4
- package/api/src/routes/connect/pay.ts +30 -20
- package/api/src/routes/connect/setup.ts +12 -4
- package/api/src/routes/connect/shared.ts +28 -4
- package/api/src/routes/connect/subscribe.ts +12 -5
- package/api/src/routes/customers.ts +5 -5
- package/api/src/routes/events.ts +9 -6
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/integrations/stripe.ts +64 -0
- package/api/src/routes/invoices.ts +19 -9
- package/api/src/routes/payment-intents.ts +19 -9
- package/api/src/routes/payment-links.ts +57 -15
- package/api/src/routes/payment-methods.ts +98 -1
- package/api/src/routes/prices.ts +71 -14
- package/api/src/routes/products.ts +79 -22
- package/api/src/routes/settings.ts +10 -11
- package/api/src/routes/subscription-items.ts +5 -5
- package/api/src/routes/subscriptions.ts +61 -10
- package/api/src/routes/usage-records.ts +52 -18
- package/api/src/routes/webhook-attempts.ts +5 -5
- package/api/src/routes/webhook-endpoints.ts +5 -5
- package/api/src/store/migrations/20230905-genesis.ts +2 -2
- package/api/src/store/migrations/20230911-seeding.ts +4 -3
- package/api/src/store/models/checkout-session.ts +15 -7
- package/api/src/store/models/index.ts +31 -7
- package/api/src/store/models/invoice.ts +1 -1
- package/api/src/store/models/payment-intent.ts +2 -5
- package/api/src/store/models/payment-link.ts +1 -1
- package/api/src/store/models/payment-method.ts +54 -33
- package/api/src/store/models/price.ts +52 -17
- package/api/src/store/models/product.ts +0 -3
- package/api/src/store/models/subscription.ts +3 -5
- package/api/src/store/models/types.ts +56 -2
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +36 -29
- package/public/currencies/dai.png +0 -0
- package/public/currencies/dollar.png +0 -0
- package/public/currencies/usdc.png +0 -0
- package/public/currencies/usdt.png +0 -0
- package/public/methods/arcblock.png +0 -0
- package/public/methods/binance.png +0 -0
- package/public/methods/coinbase.png +0 -0
- package/public/methods/ethereum.jpg +0 -0
- package/public/methods/stripe.png +0 -0
- package/src/components/checkout/form/address.tsx +86 -10
- package/src/components/checkout/form/index.tsx +169 -83
- package/src/components/checkout/form/phone.tsx +96 -0
- package/src/components/checkout/form/stripe.tsx +195 -0
- package/src/components/checkout/pay.tsx +115 -34
- package/src/components/checkout/product-item.tsx +4 -3
- package/src/components/checkout/summary.tsx +5 -4
- package/src/components/drawer-form.tsx +4 -4
- package/src/components/input.tsx +22 -4
- package/src/components/invoice/table.tsx +8 -3
- package/src/components/payment-link/before-pay.tsx +11 -6
- package/src/components/payment-link/chrome.tsx +13 -0
- package/src/components/payment-link/preview.tsx +31 -0
- package/src/components/payment-link/product-select.tsx +8 -3
- package/src/components/payment-method/arcblock.tsx +53 -0
- package/src/components/payment-method/bitcoin.tsx +53 -0
- package/src/components/payment-method/ethereum.tsx +53 -0
- package/src/components/payment-method/form.tsx +54 -0
- package/src/components/payment-method/stripe.tsx +45 -0
- package/src/components/portal/invoice/list.tsx +1 -1
- package/src/components/portal/subscription/list.tsx +1 -1
- package/src/components/price/currency-select.tsx +53 -0
- package/src/components/price/form.tsx +118 -24
- package/src/components/product/add-price.tsx +1 -1
- package/src/components/product/edit-price.tsx +6 -2
- package/src/components/subscription/items/index.tsx +7 -6
- package/src/components/subscription/items/usage-records.tsx +98 -0
- package/src/components/subscription/list.tsx +3 -2
- package/src/components/subscription/status.tsx +68 -0
- package/src/contexts/settings.tsx +2 -2
- package/src/env.d.ts +2 -0
- package/src/libs/util.ts +116 -21
- package/src/locales/en.tsx +71 -3
- package/src/pages/admin/billing/invoices/detail.tsx +5 -2
- package/src/pages/admin/billing/subscriptions/detail.tsx +6 -6
- package/src/pages/admin/customers/customers/detail.tsx +13 -1
- package/src/pages/admin/payments/intents/detail.tsx +8 -3
- package/src/pages/admin/payments/links/create.tsx +23 -3
- package/src/pages/admin/payments/links/detail.tsx +13 -26
- package/src/pages/admin/products/prices/detail.tsx +55 -11
- package/src/pages/admin/products/prices/list.tsx +7 -1
- package/src/pages/admin/products/products/create.tsx +1 -1
- package/src/pages/admin/products/products/detail.tsx +14 -7
- package/src/pages/admin/settings/index.tsx +16 -6
- package/src/pages/admin/settings/payment-methods/create.tsx +81 -0
- package/src/pages/admin/settings/{payment-methods.tsx → payment-methods/index.tsx} +9 -6
- package/src/pages/checkout/pay.tsx +3 -1
- package/src/pages/customer/index.tsx +12 -1
- package/public/.gitkeep +0 -0
|
@@ -9,10 +9,17 @@ import { subscriptionQueue } from '../../jobs/subscription';
|
|
|
9
9
|
import type { CallbackArgs } from '../../libs/auth';
|
|
10
10
|
import { wallet } from '../../libs/auth';
|
|
11
11
|
import { getClient } from '../../libs/chain/arcblock';
|
|
12
|
-
import { ensureInvoiceForCheckout, ensurePaymentIntent } from './shared';
|
|
12
|
+
import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
|
|
13
13
|
|
|
14
14
|
export default {
|
|
15
15
|
action: 'subscription',
|
|
16
|
+
authPrincipal: false,
|
|
17
|
+
claims: {
|
|
18
|
+
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
19
|
+
const { paymentMethod } = await ensurePaymentIntent(extraParams.checkoutSessionId);
|
|
20
|
+
return getAuthPrincipalClaim(paymentMethod, 'pay');
|
|
21
|
+
},
|
|
22
|
+
},
|
|
16
23
|
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
17
24
|
const { checkoutSessionId } = extraParams;
|
|
18
25
|
const { checkoutSession, paymentMethod, paymentCurrency, subscription } = await ensurePaymentIntent(
|
|
@@ -23,7 +30,6 @@ export default {
|
|
|
23
30
|
throw new Error('Subscription for checkoutSession not found');
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
// TODO: support multiple chain and multiple currency
|
|
27
33
|
if (paymentMethod.type === 'arcblock') {
|
|
28
34
|
if (checkoutSession.amount_total > '0') {
|
|
29
35
|
const client = getClient(paymentMethod.settings.arcblock?.api_host as string);
|
|
@@ -45,7 +51,6 @@ export default {
|
|
|
45
51
|
itx: {
|
|
46
52
|
address: toDelegateAddress(userDid, wallet.address),
|
|
47
53
|
to: wallet.address,
|
|
48
|
-
// FIXME: we need to enforce which token can be transferred, and how much, and at what interval on chain
|
|
49
54
|
ops: [{ typeUrl: 'fg:t:transfer_v2', rules: [] }],
|
|
50
55
|
data: {
|
|
51
56
|
type: 'json',
|
|
@@ -104,8 +109,10 @@ export default {
|
|
|
104
109
|
|
|
105
110
|
await subscription.update({
|
|
106
111
|
payment_details: {
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
arcblock: {
|
|
113
|
+
tx_hash: txHash,
|
|
114
|
+
payer: userDid,
|
|
115
|
+
},
|
|
109
116
|
},
|
|
110
117
|
});
|
|
111
118
|
|
|
@@ -11,15 +11,15 @@ const auth = authenticate<Customer>({ component: true, roles: ['owner', 'admin']
|
|
|
11
11
|
|
|
12
12
|
const schema = Joi.object<{
|
|
13
13
|
page: number;
|
|
14
|
-
|
|
14
|
+
pageSize: number;
|
|
15
15
|
livemode?: boolean;
|
|
16
16
|
}>({
|
|
17
17
|
page: Joi.number().integer().min(1).default(1),
|
|
18
|
-
|
|
18
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
19
19
|
livemode: Joi.boolean().empty(''),
|
|
20
20
|
});
|
|
21
21
|
router.get('/', auth, async (req, res) => {
|
|
22
|
-
const { page,
|
|
22
|
+
const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
|
|
23
23
|
const where: WhereOptions<Customer> = {};
|
|
24
24
|
|
|
25
25
|
if (typeof query.livemode === 'boolean') {
|
|
@@ -30,8 +30,8 @@ router.get('/', auth, async (req, res) => {
|
|
|
30
30
|
const { rows: list, count } = await Customer.findAndCountAll({
|
|
31
31
|
where,
|
|
32
32
|
order: [['created_at', 'DESC']],
|
|
33
|
-
offset: (page - 1) *
|
|
34
|
-
limit:
|
|
33
|
+
offset: (page - 1) * pageSize,
|
|
34
|
+
limit: pageSize,
|
|
35
35
|
include: [],
|
|
36
36
|
});
|
|
37
37
|
|
package/api/src/routes/events.ts
CHANGED
|
@@ -10,19 +10,19 @@ const auth = authenticate<Event>({ component: true, roles: ['owner', 'admin'] })
|
|
|
10
10
|
|
|
11
11
|
const schema = Joi.object<{
|
|
12
12
|
page: number;
|
|
13
|
-
|
|
13
|
+
pageSize: number;
|
|
14
14
|
livemode?: boolean;
|
|
15
15
|
type?: string;
|
|
16
16
|
object_id?: string;
|
|
17
17
|
}>({
|
|
18
18
|
page: Joi.number().integer().min(1).default(1),
|
|
19
|
-
|
|
19
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
20
20
|
livemode: Joi.boolean().empty(''),
|
|
21
21
|
type: Joi.string().empty(''),
|
|
22
22
|
object_id: Joi.string().empty(''),
|
|
23
23
|
});
|
|
24
24
|
router.get('/', auth, async (req, res) => {
|
|
25
|
-
const { page,
|
|
25
|
+
const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
|
|
26
26
|
const where: WhereOptions<Event> = {};
|
|
27
27
|
|
|
28
28
|
if (query.type) {
|
|
@@ -32,7 +32,10 @@ router.get('/', auth, async (req, res) => {
|
|
|
32
32
|
.filter(Boolean);
|
|
33
33
|
}
|
|
34
34
|
if (query.object_id) {
|
|
35
|
-
where.object_id = query.object_id
|
|
35
|
+
where.object_id = query.object_id
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((x) => x.trim())
|
|
38
|
+
.filter(Boolean);
|
|
36
39
|
}
|
|
37
40
|
if (typeof query.livemode === 'boolean') {
|
|
38
41
|
where.livemode = query.livemode;
|
|
@@ -43,8 +46,8 @@ router.get('/', auth, async (req, res) => {
|
|
|
43
46
|
where,
|
|
44
47
|
attributes: { exclude: ['data', 'request'] },
|
|
45
48
|
order: [['created_at', 'DESC']],
|
|
46
|
-
offset: (page - 1) *
|
|
47
|
-
limit:
|
|
49
|
+
offset: (page - 1) * pageSize,
|
|
50
|
+
limit: pageSize,
|
|
48
51
|
include: [],
|
|
49
52
|
});
|
|
50
53
|
|
package/api/src/routes/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { PaymentCurrency } from '../store/models/payment-currency';
|
|
|
4
4
|
import checkoutSessions from './checkout-sessions';
|
|
5
5
|
import customers from './customers';
|
|
6
6
|
import events from './events';
|
|
7
|
+
import stripe from './integrations/stripe';
|
|
7
8
|
import invoices from './invoices';
|
|
8
9
|
import paymentCurrencies from './payment-currencies';
|
|
9
10
|
import paymentIntents from './payment-intents';
|
|
@@ -43,6 +44,7 @@ router.use('/checkout-sessions', checkoutSessions);
|
|
|
43
44
|
router.use('/customers', customers);
|
|
44
45
|
router.use('/events', events);
|
|
45
46
|
router.use('/invoices', invoices);
|
|
47
|
+
router.use('/integrations/stripe', stripe);
|
|
46
48
|
router.use('/payment-intents', paymentIntents);
|
|
47
49
|
router.use('/payment-links', paymentLinks);
|
|
48
50
|
router.use('/payment-methods', paymentMethods);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import env from '@blocklet/sdk/lib/env';
|
|
2
|
+
import express, { NextFunction, Request, Response, Router } from 'express';
|
|
3
|
+
import get from 'lodash/get';
|
|
4
|
+
|
|
5
|
+
import handleStripeEvent from '../../integrations/stripe/handlers';
|
|
6
|
+
import logger from '../../libs/logger';
|
|
7
|
+
import { STRIPE_EVENTS } from '../../libs/util';
|
|
8
|
+
import { PaymentMethod } from '../../store/models';
|
|
9
|
+
|
|
10
|
+
const router = Router();
|
|
11
|
+
|
|
12
|
+
const verifyWebhookSig = async (req: Request, res: Response, next: NextFunction) => {
|
|
13
|
+
try {
|
|
14
|
+
const signature = req.get('stripe-signature');
|
|
15
|
+
if (!signature) {
|
|
16
|
+
return res.status(400).json({ error: 'No stripe webhook signature found' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const json = JSON.parse(req.body.toString('utf8'));
|
|
20
|
+
const method = await PaymentMethod.findOne({ where: { type: 'stripe', livemode: json.livemode } });
|
|
21
|
+
if (!method) {
|
|
22
|
+
return res.status(400).json({ error: 'No stripe payment method found' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const stripe = method.getStripe();
|
|
26
|
+
const settings = PaymentMethod.decryptSettings(method.settings);
|
|
27
|
+
const secret =
|
|
28
|
+
process.env.BLOCKLET_MODE === 'development' && process.env.STRIPE_WEBHOOK_SECRET
|
|
29
|
+
? process.env.STRIPE_WEBHOOK_SECRET
|
|
30
|
+
: settings.stripe?.webhook_signing_secret;
|
|
31
|
+
req.stripeEvent = stripe.webhooks.constructEvent(req.body, signature, secret as string);
|
|
32
|
+
req.stripeClient = stripe;
|
|
33
|
+
|
|
34
|
+
return next();
|
|
35
|
+
} catch (err) {
|
|
36
|
+
logger.error('verify signature error', { error: err });
|
|
37
|
+
return res.status(400).json({ error: err.message });
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleEvent = async (req: Request, res: Response) => {
|
|
42
|
+
const { stripeEvent, stripeClient } = req;
|
|
43
|
+
|
|
44
|
+
if (STRIPE_EVENTS.includes(stripeEvent.type) === false) {
|
|
45
|
+
logger.debug('webhook event not interested', { id: stripeEvent.id, type: stripeEvent.type });
|
|
46
|
+
return res.status(400).json({ error: 'Not implemented' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// only events from this app should be processed
|
|
50
|
+
const appPid = get(stripeEvent, 'data.object.metadata.appPid');
|
|
51
|
+
if (appPid && appPid !== env.appPid) {
|
|
52
|
+
logger.debug('webhook event for other app', { id: stripeEvent.id, type: stripeEvent.type });
|
|
53
|
+
return res.json({ received: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
logger.debug('webhook received event', { id: stripeEvent.id, type: stripeEvent.type });
|
|
57
|
+
await handleStripeEvent(stripeEvent, stripeClient);
|
|
58
|
+
|
|
59
|
+
return res.json({ received: true });
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
router.post('/webhook', express.raw({ type: 'application/json' }), verifyWebhookSig, handleEvent);
|
|
63
|
+
|
|
64
|
+
export default router;
|
|
@@ -29,7 +29,7 @@ const authPortal = authenticate<Invoice>({
|
|
|
29
29
|
|
|
30
30
|
const schema = Joi.object<{
|
|
31
31
|
page: number;
|
|
32
|
-
|
|
32
|
+
pageSize: number;
|
|
33
33
|
livemode?: boolean;
|
|
34
34
|
status?: string;
|
|
35
35
|
customer_id?: string;
|
|
@@ -37,7 +37,7 @@ const schema = Joi.object<{
|
|
|
37
37
|
subscription_id?: string;
|
|
38
38
|
}>({
|
|
39
39
|
page: Joi.number().integer().min(1).default(1),
|
|
40
|
-
|
|
40
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
41
41
|
livemode: Joi.boolean().empty(''),
|
|
42
42
|
status: Joi.string().empty(''),
|
|
43
43
|
customer_id: Joi.string().empty(''),
|
|
@@ -45,11 +45,14 @@ const schema = Joi.object<{
|
|
|
45
45
|
subscription_id: Joi.string().empty(''),
|
|
46
46
|
});
|
|
47
47
|
router.get('/', authMine, async (req, res) => {
|
|
48
|
-
const { page,
|
|
48
|
+
const { page, pageSize, livemode, status, ...query } = await schema.validateAsync(req.query, {
|
|
49
|
+
stripUnknown: false,
|
|
50
|
+
allowUnknown: true,
|
|
51
|
+
});
|
|
49
52
|
const where: WhereOptions<Invoice> = {};
|
|
50
53
|
|
|
51
|
-
if (
|
|
52
|
-
where.status =
|
|
54
|
+
if (status) {
|
|
55
|
+
where.status = status
|
|
53
56
|
.split(',')
|
|
54
57
|
.map((x) => x.trim())
|
|
55
58
|
.filter(Boolean);
|
|
@@ -66,16 +69,23 @@ router.get('/', authMine, async (req, res) => {
|
|
|
66
69
|
if (query.subscription_id) {
|
|
67
70
|
where.subscription_id = query.subscription_id;
|
|
68
71
|
}
|
|
69
|
-
if (typeof
|
|
70
|
-
where.livemode =
|
|
72
|
+
if (typeof livemode === 'boolean') {
|
|
73
|
+
where.livemode = livemode;
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
Object.keys(query)
|
|
77
|
+
.filter((x) => x.startsWith('metadata.'))
|
|
78
|
+
.forEach((key: string) => {
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
where[key] = query[key];
|
|
81
|
+
});
|
|
82
|
+
|
|
73
83
|
try {
|
|
74
84
|
const { rows: list, count } = await Invoice.findAndCountAll({
|
|
75
85
|
where,
|
|
76
86
|
order: [['created_at', 'DESC']],
|
|
77
|
-
offset: (page - 1) *
|
|
78
|
-
limit:
|
|
87
|
+
offset: (page - 1) * pageSize,
|
|
88
|
+
limit: pageSize,
|
|
79
89
|
include: [
|
|
80
90
|
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
81
91
|
// { model: PaymentMethod, as: 'paymentMethod' },
|
|
@@ -27,7 +27,7 @@ const authPortal = authenticate<PaymentIntent>({
|
|
|
27
27
|
// list payment links
|
|
28
28
|
const paginationSchema = Joi.object<{
|
|
29
29
|
page: number;
|
|
30
|
-
|
|
30
|
+
pageSize: number;
|
|
31
31
|
status?: string;
|
|
32
32
|
livemode?: boolean;
|
|
33
33
|
invoice_id?: string;
|
|
@@ -35,7 +35,7 @@ const paginationSchema = Joi.object<{
|
|
|
35
35
|
customer_did?: string;
|
|
36
36
|
}>({
|
|
37
37
|
page: Joi.number().integer().min(1).default(1),
|
|
38
|
-
|
|
38
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
39
39
|
status: Joi.string().empty(''),
|
|
40
40
|
livemode: Joi.boolean().empty(''),
|
|
41
41
|
invoice_id: Joi.string().empty(''),
|
|
@@ -43,11 +43,14 @@ const paginationSchema = Joi.object<{
|
|
|
43
43
|
customer_did: Joi.string().empty(''),
|
|
44
44
|
});
|
|
45
45
|
router.get('/', authMine, async (req, res) => {
|
|
46
|
-
const { page,
|
|
46
|
+
const { page, pageSize, status, livemode, ...query } = await paginationSchema.validateAsync(req.query, {
|
|
47
|
+
stripUnknown: false,
|
|
48
|
+
allowUnknown: true,
|
|
49
|
+
});
|
|
47
50
|
const where: WhereOptions<PaymentIntent> = {};
|
|
48
51
|
|
|
49
|
-
if (
|
|
50
|
-
where.status =
|
|
52
|
+
if (status) {
|
|
53
|
+
where.status = status
|
|
51
54
|
.split(',')
|
|
52
55
|
.map((x) => x.trim())
|
|
53
56
|
.filter(Boolean);
|
|
@@ -64,16 +67,23 @@ router.get('/', authMine, async (req, res) => {
|
|
|
64
67
|
if (query.invoice_id) {
|
|
65
68
|
where.invoice_id = query.invoice_id;
|
|
66
69
|
}
|
|
67
|
-
if (typeof
|
|
68
|
-
where.livemode =
|
|
70
|
+
if (typeof livemode === 'boolean') {
|
|
71
|
+
where.livemode = livemode;
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
Object.keys(query)
|
|
75
|
+
.filter((x) => x.startsWith('metadata.'))
|
|
76
|
+
.forEach((key: string) => {
|
|
77
|
+
// @ts-ignore
|
|
78
|
+
where[key] = query[key];
|
|
79
|
+
});
|
|
80
|
+
|
|
71
81
|
try {
|
|
72
82
|
const { rows: list, count } = await PaymentIntent.findAndCountAll({
|
|
73
83
|
where,
|
|
74
84
|
order: [['created_at', 'DESC']],
|
|
75
|
-
offset: (page - 1) *
|
|
76
|
-
limit:
|
|
85
|
+
offset: (page - 1) * pageSize,
|
|
86
|
+
limit: pageSize,
|
|
77
87
|
include: [
|
|
78
88
|
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
79
89
|
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
@@ -2,9 +2,10 @@ import { Router } from 'express';
|
|
|
2
2
|
import Joi from 'joi';
|
|
3
3
|
// import isEmpty from 'lodash/isEmpty';
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
|
-
import
|
|
5
|
+
import { Op, WhereOptions } from 'sequelize';
|
|
6
6
|
|
|
7
7
|
import { authenticate } from '../libs/security';
|
|
8
|
+
import { isLineItemAligned } from '../libs/session';
|
|
8
9
|
import { formatMetadata } from '../libs/util';
|
|
9
10
|
import { PaymentLink } from '../store/models/payment-link';
|
|
10
11
|
import { Price } from '../store/models/price';
|
|
@@ -29,7 +30,6 @@ const formatBeforeSave = (payload: any) => {
|
|
|
29
30
|
terms_of_service: 'none',
|
|
30
31
|
},
|
|
31
32
|
invoice_creation: {
|
|
32
|
-
// FIXME: force to true if we are subscription
|
|
33
33
|
enabled: false,
|
|
34
34
|
},
|
|
35
35
|
phone_number_collection: {
|
|
@@ -60,13 +60,16 @@ const formatBeforeSave = (payload: any) => {
|
|
|
60
60
|
])
|
|
61
61
|
);
|
|
62
62
|
if (raw.after_completion?.type === 'hosted_confirmation') {
|
|
63
|
-
|
|
63
|
+
// @ts-ignore
|
|
64
|
+
raw.after_completion.redirect = null;
|
|
64
65
|
}
|
|
65
66
|
if (raw.after_completion?.type === 'redirect') {
|
|
66
|
-
|
|
67
|
+
// @ts-ignore
|
|
68
|
+
raw.after_completion.hosted_confirmation = null;
|
|
67
69
|
}
|
|
68
70
|
if (!payload.include_free_trial) {
|
|
69
|
-
|
|
71
|
+
// @ts-ignore
|
|
72
|
+
raw.subscription_data = null;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
raw.line_items?.forEach((x) => {
|
|
@@ -81,8 +84,8 @@ const formatBeforeSave = (payload: any) => {
|
|
|
81
84
|
return raw;
|
|
82
85
|
};
|
|
83
86
|
|
|
84
|
-
// create payment link
|
|
85
87
|
// FIXME: @wangshijun use schema validation
|
|
88
|
+
// eslint-disable-next-line consistent-return
|
|
86
89
|
router.post('/', auth, async (req, res) => {
|
|
87
90
|
const raw: Partial<PaymentLink> = formatBeforeSave(req.body);
|
|
88
91
|
raw.active = true;
|
|
@@ -90,10 +93,26 @@ router.post('/', auth, async (req, res) => {
|
|
|
90
93
|
raw.created_via = req.user?.via;
|
|
91
94
|
raw.currency_id = raw.currency_id || req.currency.id;
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
if (!raw.line_items?.length) {
|
|
97
|
+
return res.status(400).json({ error: 'line_items should not be empty for payment link' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const items = await Price.expand(raw.line_items);
|
|
101
|
+
for (let i = 0; i < items.length; i++) {
|
|
102
|
+
const result = isLineItemAligned(items, i);
|
|
103
|
+
if (result.currency === false) {
|
|
104
|
+
return res.status(400).json({ error: 'line_items should have same currency' });
|
|
105
|
+
}
|
|
106
|
+
if (result.recurring === false) {
|
|
107
|
+
return res.status(400).json({ error: 'line_items should have same recurring' });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
94
110
|
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
if (items.some((x) => x.price.type === 'recurring')) {
|
|
112
|
+
raw.invoice_creation = { enabled: true };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const link = await PaymentLink.create(raw as PaymentLink);
|
|
97
116
|
|
|
98
117
|
res.json(link);
|
|
99
118
|
});
|
|
@@ -101,18 +120,18 @@ router.post('/', auth, async (req, res) => {
|
|
|
101
120
|
// list payment links
|
|
102
121
|
const paginationSchema = Joi.object<{
|
|
103
122
|
page: number;
|
|
104
|
-
|
|
123
|
+
pageSize: number;
|
|
105
124
|
active?: boolean;
|
|
106
125
|
livemode?: boolean;
|
|
107
126
|
}>({
|
|
108
127
|
page: Joi.number().integer().min(1).default(1),
|
|
109
|
-
|
|
128
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
110
129
|
active: Joi.boolean().empty(''),
|
|
111
130
|
livemode: Joi.boolean().empty(''),
|
|
112
131
|
});
|
|
113
132
|
router.get('/', auth, async (req, res) => {
|
|
114
|
-
const { page,
|
|
115
|
-
const where: WhereOptions<PaymentLink> = {};
|
|
133
|
+
const { page, pageSize, ...query } = await paginationSchema.validateAsync(req.query, { stripUnknown: true });
|
|
134
|
+
const where: WhereOptions<PaymentLink> = { id: { [Op.notIn]: [`plink_${req.user?.did}`] } };
|
|
116
135
|
|
|
117
136
|
if (typeof query.active === 'boolean') {
|
|
118
137
|
where.active = query.active;
|
|
@@ -125,8 +144,8 @@ router.get('/', auth, async (req, res) => {
|
|
|
125
144
|
const { rows: list, count } = await PaymentLink.findAndCountAll({
|
|
126
145
|
where,
|
|
127
146
|
order: [['created_at', 'DESC']],
|
|
128
|
-
offset: (page - 1) *
|
|
129
|
-
limit:
|
|
147
|
+
offset: (page - 1) * pageSize,
|
|
148
|
+
limit: pageSize,
|
|
130
149
|
include: [],
|
|
131
150
|
});
|
|
132
151
|
|
|
@@ -218,4 +237,27 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
218
237
|
return res.json(doc);
|
|
219
238
|
});
|
|
220
239
|
|
|
240
|
+
router.post('/stash', auth, async (req, res) => {
|
|
241
|
+
try {
|
|
242
|
+
const raw: Partial<PaymentLink> = req.body;
|
|
243
|
+
raw.id = `plink_${req.user?.did}`;
|
|
244
|
+
raw.active = true;
|
|
245
|
+
raw.livemode = !!req.livemode;
|
|
246
|
+
raw.created_via = req.user?.via;
|
|
247
|
+
raw.created_via = 'portal';
|
|
248
|
+
raw.currency_id = raw.currency_id || req.currency.id;
|
|
249
|
+
|
|
250
|
+
let doc = await PaymentLink.findByPk(raw.id);
|
|
251
|
+
if (doc) {
|
|
252
|
+
await doc.update(formatBeforeSave(req.body));
|
|
253
|
+
} else {
|
|
254
|
+
doc = await PaymentLink.create(raw as PaymentLink);
|
|
255
|
+
}
|
|
256
|
+
res.json(doc);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error(err);
|
|
259
|
+
res.status(500).json({ error: err.message });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
221
263
|
export default router;
|
|
@@ -1,14 +1,94 @@
|
|
|
1
|
+
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
1
2
|
import { Router } from 'express';
|
|
2
3
|
import { InferAttributes, Op, WhereOptions } from 'sequelize';
|
|
3
4
|
|
|
5
|
+
import { ensureWebhookRegistered } from '../integrations/stripe/setup';
|
|
4
6
|
import { authenticate } from '../libs/security';
|
|
5
7
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
6
|
-
import { PaymentMethod } from '../store/models/payment-method';
|
|
8
|
+
import { PaymentMethod, TPaymentMethod } from '../store/models/payment-method';
|
|
7
9
|
|
|
8
10
|
const router = Router();
|
|
9
11
|
|
|
10
12
|
const auth = authenticate<PaymentMethod>({ component: true, roles: ['owner', 'admin'] });
|
|
11
13
|
|
|
14
|
+
router.post('/', auth, async (req, res) => {
|
|
15
|
+
const raw: Partial<TPaymentMethod> = req.body;
|
|
16
|
+
|
|
17
|
+
raw.livemode = req.livemode;
|
|
18
|
+
raw.locked = false;
|
|
19
|
+
raw.active = true;
|
|
20
|
+
|
|
21
|
+
if (!raw.name) {
|
|
22
|
+
return res.status(400).json({ error: 'payment method name is required' });
|
|
23
|
+
}
|
|
24
|
+
if (!raw.description) {
|
|
25
|
+
return res.status(400).json({ error: 'payment method description is required' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!raw.settings) {
|
|
29
|
+
return res.status(400).json({ error: 'payment method settings is required' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (raw.type === 'stripe') {
|
|
33
|
+
const exist = await PaymentMethod.findOne({ where: { type: 'stripe', livemode: raw.livemode } });
|
|
34
|
+
if (exist) {
|
|
35
|
+
return res.status(400).json({ error: 'stripe payment method already exist' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!raw.settings.stripe?.publishable_key) {
|
|
39
|
+
return res.status(400).json({ error: 'stripe publishable key is required' });
|
|
40
|
+
}
|
|
41
|
+
if (!raw.settings.stripe?.secret_key) {
|
|
42
|
+
return res.status(400).json({ error: 'stripe secret key is required' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
raw.settings = PaymentMethod.encryptSettings(raw.settings);
|
|
46
|
+
raw.logo = getUrl('/methods/stripe.png');
|
|
47
|
+
raw.features = {
|
|
48
|
+
recurring: true,
|
|
49
|
+
refund: true,
|
|
50
|
+
dispute: true,
|
|
51
|
+
};
|
|
52
|
+
raw.confirmation = {
|
|
53
|
+
type: 'callback',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const method = await PaymentMethod.create(raw as TPaymentMethod);
|
|
57
|
+
|
|
58
|
+
// create default currency
|
|
59
|
+
// FIXME: make this configurable
|
|
60
|
+
const currency = await PaymentCurrency.create({
|
|
61
|
+
livemode: method.livemode,
|
|
62
|
+
active: method.active,
|
|
63
|
+
locked: false,
|
|
64
|
+
is_base_currency: false,
|
|
65
|
+
payment_method_id: method.id,
|
|
66
|
+
|
|
67
|
+
name: 'Dollar',
|
|
68
|
+
description: 'US Dollar',
|
|
69
|
+
logo: getUrl('/currencies/dollar.png'),
|
|
70
|
+
symbol: 'USD', // same currency code as stripe
|
|
71
|
+
decimal: 2,
|
|
72
|
+
|
|
73
|
+
minimum_payment_amount: '1', // cent
|
|
74
|
+
maximum_payment_amount: '100000000000', // billion
|
|
75
|
+
|
|
76
|
+
contract: '',
|
|
77
|
+
metadata: {},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await method.update({ default_currency_id: currency.id });
|
|
81
|
+
|
|
82
|
+
ensureWebhookRegistered().catch(console.error);
|
|
83
|
+
|
|
84
|
+
return res.json({ ...method.toJSON(), payment_currencies: [currency.toJSON()] });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// FIXME: support add more payment methods
|
|
88
|
+
|
|
89
|
+
return res.status(400).json({ error: 'payment method type is not supported' });
|
|
90
|
+
});
|
|
91
|
+
|
|
12
92
|
router.get('/', auth, async (req, res) => {
|
|
13
93
|
const { query } = req;
|
|
14
94
|
const where: WhereOptions<InferAttributes<PaymentMethod>> = {};
|
|
@@ -28,6 +108,23 @@ router.get('/', auth, async (req, res) => {
|
|
|
28
108
|
res.json(list);
|
|
29
109
|
});
|
|
30
110
|
|
|
111
|
+
router.get('/types', auth, (_, res) => {
|
|
112
|
+
res.json([
|
|
113
|
+
{
|
|
114
|
+
type: 'arcblock',
|
|
115
|
+
name: 'ArcBlock',
|
|
116
|
+
description: 'Instant payment with ArcBlock chain',
|
|
117
|
+
logo: getUrl('/methods/arcblock.png'),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: 'stripe',
|
|
121
|
+
name: 'Stripe',
|
|
122
|
+
description: 'Pay with credit card or bank account',
|
|
123
|
+
logo: getUrl('/methods/stripe.png'),
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
|
|
31
128
|
router.get('/:id', auth, async (req, res) => {
|
|
32
129
|
const doc = await PaymentMethod.findOne({
|
|
33
130
|
where: { [Op.or]: [{ id: req.params.id }, { name: req.params.id }] },
|