payment-kit 1.13.210 → 1.13.211
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/libs/api.ts +2 -2
- package/api/src/libs/session.ts +90 -4
- package/api/src/queues/payment.ts +61 -1
- package/api/src/routes/checkout-sessions.ts +61 -10
- package/api/src/routes/connect/collect.ts +44 -37
- package/api/src/routes/connect/pay.ts +40 -29
- package/api/src/routes/connect/setup.ts +39 -33
- package/api/src/routes/connect/shared.ts +3 -1
- package/api/src/routes/donations.ts +157 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/payment-intents.ts +2 -2
- package/api/src/routes/payment-links.ts +8 -3
- package/api/src/routes/payouts.ts +151 -0
- package/api/src/routes/products.ts +24 -6
- package/api/src/routes/usage-records.ts +6 -3
- package/api/src/store/migrations/20240408-payout.ts +36 -0
- package/api/src/store/models/checkout-session.ts +5 -0
- package/api/src/store/models/customer.ts +6 -1
- package/api/src/store/models/index.ts +12 -0
- package/api/src/store/models/payment-intent.ts +38 -26
- package/api/src/store/models/payment-link.ts +8 -1
- package/api/src/store/models/payout.ts +243 -0
- package/api/src/store/models/types.ts +39 -0
- package/api/tests/libs/session.spec.ts +101 -0
- package/blocklet.yml +1 -1
- package/package.json +17 -16
- package/src/components/info-card.tsx +5 -5
- package/src/components/invoice/list.tsx +2 -0
- package/src/components/invoice/table.tsx +1 -1
- package/src/components/payment-intent/list.tsx +2 -0
- package/src/components/payouts/actions.tsx +43 -0
- package/src/components/payouts/list.tsx +255 -0
- package/src/components/refund/list.tsx +2 -0
- package/src/components/subscription/list.tsx +2 -0
- package/src/libs/util.ts +4 -1
- package/src/locales/en.tsx +7 -0
- package/src/locales/zh.tsx +6 -0
- package/src/pages/admin/customers/customers/index.tsx +2 -2
- package/src/pages/admin/payments/index.tsx +7 -0
- package/src/pages/admin/payments/intents/detail.tsx +7 -0
- package/src/pages/admin/payments/payouts/detail.tsx +204 -0
- package/src/pages/admin/payments/payouts/index.tsx +5 -0
- package/src/pages/admin/products/links/index.tsx +2 -2
- package/src/pages/admin/products/prices/detail.tsx +2 -1
- package/src/pages/admin/products/pricing-tables/index.tsx +2 -2
- package/src/pages/admin/products/products/index.tsx +2 -2
|
@@ -99,42 +99,48 @@ export default {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
if (paymentMethod.type === 'arcblock') {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
102
|
+
try {
|
|
103
|
+
const paymentSettings = {
|
|
104
|
+
payment_method_types: ['arcblock'],
|
|
105
|
+
payment_method_options: {
|
|
106
|
+
arcblock: { payer: userDid },
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
await setupIntent.update({ status: 'processing' });
|
|
110
|
+
await subscription.update({ payment_settings: paymentSettings });
|
|
111
|
+
if (invoice) {
|
|
112
|
+
await invoice.update({ payment_settings: paymentSettings });
|
|
113
|
+
}
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
const paymentDetails = await executeOcapTransactions(userDid, userPk, claims, paymentMethod);
|
|
116
|
+
await setupIntent.update({
|
|
117
|
+
status: 'succeeded',
|
|
118
|
+
last_setup_error: null,
|
|
119
|
+
setup_details: { arcblock: paymentDetails },
|
|
120
|
+
...paymentSettings,
|
|
121
|
+
});
|
|
122
|
+
await subscription.update({
|
|
123
|
+
status: subscription.trial_end ? 'trialing' : 'active',
|
|
124
|
+
payment_details: { arcblock: paymentDetails },
|
|
125
|
+
});
|
|
125
126
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
127
|
+
await checkoutSession.update({ status: 'complete', payment_status: 'paid' });
|
|
128
|
+
if (invoice) {
|
|
129
|
+
invoiceQueue.pushAndWait({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
|
|
130
|
+
}
|
|
131
|
+
await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
|
|
132
|
+
logger.info('CheckoutSession updated on setup done', {
|
|
133
|
+
checkoutSession: checkoutSession.id,
|
|
134
|
+
setupIntent: setupIntent.id,
|
|
135
|
+
paymentDetails,
|
|
136
|
+
});
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
return { hash: paymentDetails.tx_hash };
|
|
139
|
+
} catch (err) {
|
|
140
|
+
logger.error('Failed to finalize setup', { setupIntent: setupIntent.id, error: err });
|
|
141
|
+
await setupIntent.update({ status: 'requires_capture' });
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
@@ -454,7 +454,9 @@ export async function ensureInvoiceAndItems({
|
|
|
454
454
|
|
|
455
455
|
return {
|
|
456
456
|
price,
|
|
457
|
-
amount:
|
|
457
|
+
amount:
|
|
458
|
+
x.custom_amount ||
|
|
459
|
+
new BN(getPriceUintAmountByCurrency(price, props.currency_id)).mul(new BN(x.quantity)).toString(),
|
|
458
460
|
// @ts-ignore
|
|
459
461
|
description: price.product.name,
|
|
460
462
|
period: undefined,
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Joi } from '@arcblock/validator';
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
|
|
4
|
+
import { createListParamSchema } from '../libs/api';
|
|
5
|
+
import logger from '../libs/logger';
|
|
6
|
+
import { CheckoutSession } from '../store/models/checkout-session';
|
|
7
|
+
import { Customer } from '../store/models/customer';
|
|
8
|
+
import { PaymentLink } from '../store/models/payment-link';
|
|
9
|
+
import { PaymentMethod } from '../store/models/payment-method';
|
|
10
|
+
import { Price } from '../store/models/price';
|
|
11
|
+
import type { DonationSettings } from '../store/models/types';
|
|
12
|
+
import { createPaymentLink } from './payment-links';
|
|
13
|
+
import { createProductAndPrices } from './products';
|
|
14
|
+
|
|
15
|
+
const router = Router();
|
|
16
|
+
|
|
17
|
+
// FIXME: add more custom validation here for amount
|
|
18
|
+
const donationSchema = Joi.object<DonationSettings>({
|
|
19
|
+
target: Joi.string().max(128).required(),
|
|
20
|
+
title: Joi.string().required(),
|
|
21
|
+
description: Joi.string().required(),
|
|
22
|
+
reference: Joi.string().required(),
|
|
23
|
+
beneficiaries: Joi.array()
|
|
24
|
+
.items(
|
|
25
|
+
Joi.object({
|
|
26
|
+
address: Joi.DID().required(),
|
|
27
|
+
share: Joi.number().positive().required(),
|
|
28
|
+
memo: Joi.string().max(64).optional(),
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
.max(8)
|
|
32
|
+
.optional()
|
|
33
|
+
.default([]),
|
|
34
|
+
amount: Joi.object({
|
|
35
|
+
presets: Joi.array().items(Joi.number().positive()).optional().default([]),
|
|
36
|
+
preset: Joi.number().positive().optional(),
|
|
37
|
+
minimum: Joi.number().positive().optional(),
|
|
38
|
+
maximum: Joi.number().positive().optional(),
|
|
39
|
+
custom: Joi.boolean().optional().default(true),
|
|
40
|
+
}),
|
|
41
|
+
message: Joi.object({
|
|
42
|
+
success: Joi.string().optional(),
|
|
43
|
+
summary: Joi.string().optional(),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
// prepare donation payment links
|
|
47
|
+
router.post('/', async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const payload = await donationSchema.validateAsync(req.body, { stripUnknown: true, convert: true });
|
|
50
|
+
const link = await PaymentLink.findOne({ where: { 'donation_settings.target': payload.target } });
|
|
51
|
+
if (link) {
|
|
52
|
+
await link.update({
|
|
53
|
+
name: payload.title,
|
|
54
|
+
submit_type: 'donate',
|
|
55
|
+
donation_settings: payload,
|
|
56
|
+
after_completion: {
|
|
57
|
+
type: 'hosted_confirmation',
|
|
58
|
+
hosted_confirmation: {
|
|
59
|
+
custom_message: payload.message?.success || '',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
res.json(link.toJSON());
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let price = await Price.findByPkOrLookupKey(payload.target);
|
|
68
|
+
if (!price) {
|
|
69
|
+
const result = await createProductAndPrices({
|
|
70
|
+
type: 'service',
|
|
71
|
+
livemode: req.livemode,
|
|
72
|
+
name: payload.title,
|
|
73
|
+
description: payload.description,
|
|
74
|
+
currency_id: req.currency.id,
|
|
75
|
+
prices: [
|
|
76
|
+
{
|
|
77
|
+
type: 'one_time',
|
|
78
|
+
unit_amount: '0',
|
|
79
|
+
billing_schema: 'per_unit',
|
|
80
|
+
lookup_key: payload.target,
|
|
81
|
+
custom_unit_amount: {
|
|
82
|
+
presets: payload.amount.presets || [],
|
|
83
|
+
preset: payload.amount.preset || null,
|
|
84
|
+
maximum: payload.amount.maximum || null,
|
|
85
|
+
minimum: payload.amount.minimum || '0',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
metadata: {},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
price = result.prices[0] as Price;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const result = await createPaymentLink({
|
|
96
|
+
livemode: !!req.livemode,
|
|
97
|
+
created_via: req.user?.via,
|
|
98
|
+
currency_id: req.currency.id,
|
|
99
|
+
name: payload.title,
|
|
100
|
+
submit_type: 'donate',
|
|
101
|
+
line_items: [{ price_id: price.id, quantity: 1 }],
|
|
102
|
+
donation_settings: payload,
|
|
103
|
+
after_completion: {
|
|
104
|
+
type: 'hosted_confirmation',
|
|
105
|
+
hosted_confirmation: {
|
|
106
|
+
custom_message: payload.message?.success || '',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
res.json(result);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
logger.error('prepare payment link for donation', err);
|
|
113
|
+
res.status(400).json({ error: err.message });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// get donations by target
|
|
118
|
+
const paginationSchema = createListParamSchema<{ target: string }>({ target: Joi.string().required() }, 20);
|
|
119
|
+
router.get('/', async (req, res) => {
|
|
120
|
+
try {
|
|
121
|
+
const { page, pageSize, target } = await paginationSchema.validateAsync(req.query, {
|
|
122
|
+
convert: true,
|
|
123
|
+
stripUnknown: true,
|
|
124
|
+
});
|
|
125
|
+
const { rows, count } = await CheckoutSession.findAndCountAll({
|
|
126
|
+
where: { payment_link_id: target, status: 'complete' },
|
|
127
|
+
attributes: [
|
|
128
|
+
'id',
|
|
129
|
+
'customer_id',
|
|
130
|
+
'customer_did',
|
|
131
|
+
'amount_total',
|
|
132
|
+
'payment_intent_id',
|
|
133
|
+
'payment_details',
|
|
134
|
+
'created_at',
|
|
135
|
+
'updated_at',
|
|
136
|
+
],
|
|
137
|
+
order: [['created_at', 'DESC']],
|
|
138
|
+
offset: (page - 1) * pageSize,
|
|
139
|
+
include: [{ model: Customer, as: 'customer', attributes: ['id', 'did', 'name'] }],
|
|
140
|
+
limit: pageSize,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const method = await PaymentMethod.findByPk(req.currency.payment_method_id);
|
|
144
|
+
|
|
145
|
+
res.json({
|
|
146
|
+
supporters: rows,
|
|
147
|
+
currency: req.currency,
|
|
148
|
+
method,
|
|
149
|
+
total: count,
|
|
150
|
+
paging: { page, pageSize },
|
|
151
|
+
});
|
|
152
|
+
} catch (err) {
|
|
153
|
+
res.status(400).json({ error: err.message, supporters: [] });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
export default router;
|
package/api/src/routes/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Router } from 'express';
|
|
|
3
3
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
4
4
|
import checkoutSessions from './checkout-sessions';
|
|
5
5
|
import customers from './customers';
|
|
6
|
+
import donations from './donations';
|
|
6
7
|
import events from './events';
|
|
7
8
|
import stripe from './integrations/stripe';
|
|
8
9
|
import invoices from './invoices';
|
|
@@ -11,6 +12,7 @@ import paymentCurrencies from './payment-currencies';
|
|
|
11
12
|
import paymentIntents from './payment-intents';
|
|
12
13
|
import paymentLinks from './payment-links';
|
|
13
14
|
import paymentMethods from './payment-methods';
|
|
15
|
+
import payouts from './payouts';
|
|
14
16
|
import prices from './prices';
|
|
15
17
|
import pricingTables from './pricing-table';
|
|
16
18
|
import products from './products';
|
|
@@ -46,6 +48,7 @@ router.use(async (req, _, next) => {
|
|
|
46
48
|
|
|
47
49
|
router.use('/checkout-sessions', checkoutSessions);
|
|
48
50
|
router.use('/customers', customers);
|
|
51
|
+
router.use('/donations', donations);
|
|
49
52
|
router.use('/events', events);
|
|
50
53
|
router.use('/invoices', invoices);
|
|
51
54
|
router.use('/integrations/stripe', stripe);
|
|
@@ -57,6 +60,7 @@ router.use('/payment-currencies', paymentCurrencies);
|
|
|
57
60
|
router.use('/prices', prices);
|
|
58
61
|
router.use('/pricing-tables', pricingTables);
|
|
59
62
|
router.use('/products', products);
|
|
63
|
+
router.use('/payouts', payouts);
|
|
60
64
|
router.use('/redirect', redirect);
|
|
61
65
|
router.use('/refunds', refunds);
|
|
62
66
|
router.use('/settings', settings);
|
|
@@ -17,8 +17,8 @@ import { PaymentMethod } from '../store/models/payment-method';
|
|
|
17
17
|
import { Subscription } from '../store/models/subscription';
|
|
18
18
|
|
|
19
19
|
const router = Router();
|
|
20
|
-
const authAdmin = authenticate<
|
|
21
|
-
const authMine = authenticate<
|
|
20
|
+
const authAdmin = authenticate<PaymentIntent>({ component: true, roles: ['owner', 'admin'] });
|
|
21
|
+
const authMine = authenticate<PaymentIntent>({ component: true, roles: ['owner', 'admin'], mine: true });
|
|
22
22
|
const authPortal = authenticate<PaymentIntent>({
|
|
23
23
|
component: true,
|
|
24
24
|
roles: ['owner', 'admin'],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { Joi } from '@arcblock/validator';
|
|
1
2
|
import { Router } from 'express';
|
|
2
|
-
import Joi from 'joi';
|
|
3
3
|
import pick from 'lodash/pick';
|
|
4
4
|
import type { WhereOptions } from 'sequelize';
|
|
5
5
|
|
|
@@ -31,7 +31,7 @@ const formatBeforeSave = (payload: any) => {
|
|
|
31
31
|
terms_of_service: 'none',
|
|
32
32
|
},
|
|
33
33
|
invoice_creation: {
|
|
34
|
-
enabled:
|
|
34
|
+
enabled: true,
|
|
35
35
|
},
|
|
36
36
|
phone_number_collection: {
|
|
37
37
|
enabled: false,
|
|
@@ -47,6 +47,7 @@ const formatBeforeSave = (payload: any) => {
|
|
|
47
47
|
},
|
|
48
48
|
submit_type: 'pay',
|
|
49
49
|
cross_sell_behavior: 'auto',
|
|
50
|
+
donation_settings: null,
|
|
50
51
|
},
|
|
51
52
|
pick(payload, [
|
|
52
53
|
'name',
|
|
@@ -64,6 +65,7 @@ const formatBeforeSave = (payload: any) => {
|
|
|
64
65
|
'subscription_data',
|
|
65
66
|
'nft_mint_settings',
|
|
66
67
|
'cross_sell_behavior',
|
|
68
|
+
'donation_settings',
|
|
67
69
|
'metadata',
|
|
68
70
|
])
|
|
69
71
|
);
|
|
@@ -110,6 +112,10 @@ export async function createPaymentLink(payload: any) {
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
const items = await Price.expand(raw.line_items);
|
|
115
|
+
if (items.find((x) => x.price.custom_unit_amount) && items.length > 1) {
|
|
116
|
+
throw new Error('Multiple items with custom unit amount are not supported in payment link');
|
|
117
|
+
}
|
|
118
|
+
|
|
113
119
|
for (let i = 0; i < items.length; i++) {
|
|
114
120
|
const result = isLineItemAligned(items, i);
|
|
115
121
|
if (result.currency === false) {
|
|
@@ -128,7 +134,6 @@ export async function createPaymentLink(payload: any) {
|
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
// FIXME: @wangshijun use schema validation
|
|
131
|
-
// eslint-disable-next-line consistent-return
|
|
132
137
|
router.post('/', auth, async (req, res) => {
|
|
133
138
|
try {
|
|
134
139
|
const result = await createPaymentLink({
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { isValid } from '@arcblock/did';
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
import Joi from 'joi';
|
|
4
|
+
import pick from 'lodash/pick';
|
|
5
|
+
|
|
6
|
+
import { createListParamSchema, getWhereFromKvQuery } from '../libs/api';
|
|
7
|
+
import { authenticate } from '../libs/security';
|
|
8
|
+
import { formatMetadata } from '../libs/util';
|
|
9
|
+
import { Customer } from '../store/models/customer';
|
|
10
|
+
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
11
|
+
import { PaymentIntent } from '../store/models/payment-intent';
|
|
12
|
+
import { PaymentMethod } from '../store/models/payment-method';
|
|
13
|
+
import { Payout } from '../store/models/payout';
|
|
14
|
+
|
|
15
|
+
const router = Router();
|
|
16
|
+
const authAdmin = authenticate<Payout>({ component: true, roles: ['owner', 'admin'] });
|
|
17
|
+
const authMine = authenticate<Payout>({ component: true, roles: ['owner', 'admin'], mine: true });
|
|
18
|
+
const authPortal = authenticate<Payout>({
|
|
19
|
+
component: true,
|
|
20
|
+
roles: ['owner', 'admin'],
|
|
21
|
+
record: {
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
model: Payout,
|
|
24
|
+
field: 'customer_id',
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// list payment intents
|
|
29
|
+
const paginationSchema = createListParamSchema<{
|
|
30
|
+
status?: string;
|
|
31
|
+
payment_intent_id?: string;
|
|
32
|
+
customer_id?: string;
|
|
33
|
+
customer_did?: string;
|
|
34
|
+
currency_id?: string;
|
|
35
|
+
}>({
|
|
36
|
+
status: Joi.string().empty(''),
|
|
37
|
+
payment_intent_id: Joi.string().empty(''),
|
|
38
|
+
customer_id: Joi.string().empty(''),
|
|
39
|
+
customer_did: Joi.string().empty(''),
|
|
40
|
+
currency_id: Joi.string().empty(''),
|
|
41
|
+
});
|
|
42
|
+
router.get('/', authMine, async (req, res) => {
|
|
43
|
+
const { page, pageSize, status, livemode, ...query } = await paginationSchema.validateAsync(req.query, {
|
|
44
|
+
stripUnknown: false,
|
|
45
|
+
allowUnknown: true,
|
|
46
|
+
});
|
|
47
|
+
const where = getWhereFromKvQuery(query.q);
|
|
48
|
+
|
|
49
|
+
if (status) {
|
|
50
|
+
where.status = status
|
|
51
|
+
.split(',')
|
|
52
|
+
.map((x) => x.trim())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
if (query.customer_id) {
|
|
56
|
+
where.customer_id = query.customer_id;
|
|
57
|
+
}
|
|
58
|
+
if (query.customer_did && isValid(query.customer_did)) {
|
|
59
|
+
const customer = await Customer.findOne({ where: { did: query.customer_did } });
|
|
60
|
+
if (customer) {
|
|
61
|
+
where.customer_id = customer.id;
|
|
62
|
+
} else {
|
|
63
|
+
res.json({ count: 0, list: [] });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (query.payment_intent_id) {
|
|
68
|
+
where.payment_intent_id = query.payment_intent_id;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (query.currency_id) {
|
|
72
|
+
where.currency_id = query.currency_id;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof livemode === 'boolean') {
|
|
76
|
+
where.livemode = livemode;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Object.keys(query)
|
|
80
|
+
.filter((x) => x.startsWith('metadata.'))
|
|
81
|
+
.forEach((key: string) => {
|
|
82
|
+
// @ts-ignore
|
|
83
|
+
where[key] = query[key];
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const { rows: list, count } = await Payout.findAndCountAll({
|
|
88
|
+
where,
|
|
89
|
+
order: [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']],
|
|
90
|
+
offset: (page - 1) * pageSize,
|
|
91
|
+
limit: pageSize,
|
|
92
|
+
include: [
|
|
93
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
94
|
+
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
95
|
+
{ model: PaymentIntent, as: 'paymentIntent' },
|
|
96
|
+
{ model: Customer, as: 'customer' },
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
res.json({ count, list, paging: { page, pageSize } });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(err);
|
|
103
|
+
res.json({ count: 0, list: [], paging: { page, pageSize } });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
router.get('/:id', authPortal, async (req, res) => {
|
|
108
|
+
try {
|
|
109
|
+
const doc = await Payout.findOne({
|
|
110
|
+
where: { id: req.params.id },
|
|
111
|
+
include: [
|
|
112
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
113
|
+
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
114
|
+
{ model: PaymentIntent, as: 'paymentIntent' },
|
|
115
|
+
{ model: Customer, as: 'customer' },
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (doc) {
|
|
120
|
+
res.json({ ...doc.toJSON() });
|
|
121
|
+
} else {
|
|
122
|
+
res.status(404).json(null);
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(err);
|
|
126
|
+
res.status(500).json({ error: `Failed to get payout: ${err.message}` });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// eslint-disable-next-line consistent-return
|
|
131
|
+
router.put('/:id', authAdmin, async (req, res) => {
|
|
132
|
+
try {
|
|
133
|
+
const doc = await Payout.findByPk(req.params.id as string);
|
|
134
|
+
if (!doc) {
|
|
135
|
+
return res.status(404).json({ error: 'Payout not found' });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const raw = pick(req.body, ['metadata']);
|
|
139
|
+
if (raw.metadata) {
|
|
140
|
+
raw.metadata = formatMetadata(raw.metadata);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await doc.update(raw);
|
|
144
|
+
res.json(doc);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error(err);
|
|
147
|
+
res.json(null);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export default router;
|
|
@@ -10,6 +10,7 @@ import { formatMetadata } from '../libs/util';
|
|
|
10
10
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
11
11
|
import { Price } from '../store/models/price';
|
|
12
12
|
import { Product } from '../store/models/product';
|
|
13
|
+
import type { CustomUnitAmount } from '../store/models/types';
|
|
13
14
|
|
|
14
15
|
const router = Router();
|
|
15
16
|
|
|
@@ -36,8 +37,8 @@ export async function createProductAndPrices(payload: any) {
|
|
|
36
37
|
|
|
37
38
|
const product = await Product.create(raw as Product);
|
|
38
39
|
if (Array.isArray(payload.prices) && payload.prices.length) {
|
|
39
|
-
if (payload.prices.some((x: any) => !x.unit_amount)) {
|
|
40
|
-
throw new Error('unit_amount is required for price');
|
|
40
|
+
if (payload.prices.some((x: any) => !x.unit_amount && !x.custom_unit_amount)) {
|
|
41
|
+
throw new Error('unit_amount or custom_unit_amount is required for price');
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
const currencies = await PaymentCurrency.findAll({ where: { active: true } });
|
|
@@ -51,10 +52,27 @@ export async function createProductAndPrices(payload: any) {
|
|
|
51
52
|
if (!currency) {
|
|
52
53
|
throw new Error(`currency ${price.currency_id} used in price not found or inactive`);
|
|
53
54
|
}
|
|
54
|
-
if (
|
|
55
|
-
|
|
55
|
+
if (price.custom_unit_amount) {
|
|
56
|
+
// @ts-ignore
|
|
57
|
+
['preset', 'maximum', 'minimum'].forEach((key: keyof CustomUnitAmount) => {
|
|
58
|
+
if (price.custom_unit_amount?.[key]) {
|
|
59
|
+
price.custom_unit_amount[key] = fromTokenToUnit(
|
|
60
|
+
price.custom_unit_amount[key] as string,
|
|
61
|
+
currency.decimal
|
|
62
|
+
).toString();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
if (Array.isArray(price.custom_unit_amount.presets)) {
|
|
66
|
+
price.custom_unit_amount.presets = price.custom_unit_amount.presets.map((x) =>
|
|
67
|
+
fromTokenToUnit(x, currency.decimal).toString()
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
if (!price.unit_amount) {
|
|
72
|
+
throw new Error('price.unit_amount is required');
|
|
73
|
+
}
|
|
74
|
+
price.unit_amount = fromTokenToUnit(price.unit_amount, currency.decimal).toString();
|
|
56
75
|
}
|
|
57
|
-
price.unit_amount = fromTokenToUnit(price.unit_amount, currency.decimal).toString();
|
|
58
76
|
|
|
59
77
|
if (Array.isArray(price.currency_options)) {
|
|
60
78
|
price.currency_options = Price.formatCurrencies(price.currency_options, currencies);
|
|
@@ -66,7 +84,7 @@ export async function createProductAndPrices(payload: any) {
|
|
|
66
84
|
currency_id: price.currency_id,
|
|
67
85
|
unit_amount: price.unit_amount,
|
|
68
86
|
tiers: null,
|
|
69
|
-
custom_unit_amount:
|
|
87
|
+
custom_unit_amount: price.custom_unit_amount,
|
|
70
88
|
});
|
|
71
89
|
}
|
|
72
90
|
|
|
@@ -78,9 +78,12 @@ router.post('/', auth, async (req, res) => {
|
|
|
78
78
|
// @link https://stripe.com/docs/api/usage_records/subscription_item_summary_list
|
|
79
79
|
const schema = createListParamSchema<{
|
|
80
80
|
subscription_item_id: string;
|
|
81
|
-
}>(
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
}>(
|
|
82
|
+
{
|
|
83
|
+
subscription_item_id: Joi.string().required(),
|
|
84
|
+
},
|
|
85
|
+
100
|
|
86
|
+
);
|
|
84
87
|
router.get('/summary', auth, async (req, res) => {
|
|
85
88
|
const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
|
|
86
89
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
import { Migration, safeApplyColumnChanges } from '../migrate';
|
|
4
|
+
import models from '../models';
|
|
5
|
+
|
|
6
|
+
export const up: Migration = async ({ context }) => {
|
|
7
|
+
await context.createTable('payouts', models.Payout.GENESIS_ATTRIBUTES);
|
|
8
|
+
await safeApplyColumnChanges(context, {
|
|
9
|
+
payment_links: [
|
|
10
|
+
{
|
|
11
|
+
name: 'donation_settings',
|
|
12
|
+
field: {
|
|
13
|
+
type: DataTypes.JSON,
|
|
14
|
+
allowNull: true,
|
|
15
|
+
defaultValue: {},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
payment_intents: [
|
|
20
|
+
{
|
|
21
|
+
name: 'beneficiaries',
|
|
22
|
+
field: {
|
|
23
|
+
type: DataTypes.JSON,
|
|
24
|
+
allowNull: true,
|
|
25
|
+
defaultValue: [],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const down: Migration = async ({ context }) => {
|
|
33
|
+
await context.dropTable('payouts');
|
|
34
|
+
await context.removeColumn('payment_links', 'donation_settings');
|
|
35
|
+
await context.removeColumn('payment_intents', 'beneficiaries');
|
|
36
|
+
};
|
|
@@ -463,6 +463,11 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
|
|
|
463
463
|
foreignKey: 'id',
|
|
464
464
|
as: 'currency',
|
|
465
465
|
});
|
|
466
|
+
this.hasOne(models.Customer, {
|
|
467
|
+
sourceKey: 'customer_id',
|
|
468
|
+
foreignKey: 'id',
|
|
469
|
+
as: 'customer',
|
|
470
|
+
});
|
|
466
471
|
}
|
|
467
472
|
|
|
468
473
|
public static findByPkOrClientRefId(id: string, options: FindOptions<CheckoutSession> = {}) {
|
|
@@ -15,10 +15,11 @@ import {
|
|
|
15
15
|
import { createEvent } from '../../libs/audit';
|
|
16
16
|
import CustomError from '../../libs/error';
|
|
17
17
|
import { getLock } from '../../libs/lock';
|
|
18
|
-
import { createIdGenerator } from '../../libs/util';
|
|
18
|
+
import { createCodeGenerator, createIdGenerator } from '../../libs/util';
|
|
19
19
|
import type { CustomerAddress, CustomerShipping } from './types';
|
|
20
20
|
|
|
21
21
|
export const nextCustomerId = createIdGenerator('cus', 14);
|
|
22
|
+
export const nextInvoicePrefix = createCodeGenerator('', 8);
|
|
22
23
|
|
|
23
24
|
// eslint-disable-next-line prettier/prettier
|
|
24
25
|
export class Customer extends Model<InferAttributes<Customer>, InferCreationAttributes<Customer>> {
|
|
@@ -271,6 +272,10 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
|
|
|
271
272
|
...options,
|
|
272
273
|
});
|
|
273
274
|
}
|
|
275
|
+
|
|
276
|
+
public static getInvoicePrefix() {
|
|
277
|
+
return nextInvoicePrefix();
|
|
278
|
+
}
|
|
274
279
|
}
|
|
275
280
|
|
|
276
281
|
export type TCustomer = InferAttributes<Customer>;
|