payment-kit 1.14.30 → 1.14.31
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 +4 -0
- package/api/src/libs/api.ts +23 -0
- package/api/src/libs/subscription.ts +32 -0
- package/api/src/queues/refund.ts +38 -1
- package/api/src/queues/subscription.ts +218 -21
- package/api/src/routes/checkout-sessions.ts +5 -0
- package/api/src/routes/customers.ts +27 -1
- package/api/src/routes/invoices.ts +5 -1
- package/api/src/routes/payment-intents.ts +17 -2
- package/api/src/routes/payment-links.ts +105 -3
- package/api/src/routes/payouts.ts +5 -1
- package/api/src/routes/prices.ts +19 -3
- package/api/src/routes/pricing-table.ts +79 -2
- package/api/src/routes/products.ts +24 -8
- package/api/src/routes/refunds.ts +7 -4
- package/api/src/routes/subscription-items.ts +5 -1
- package/api/src/routes/subscriptions.ts +25 -5
- package/api/src/routes/webhook-endpoints.ts +5 -1
- package/api/src/store/models/subscription.ts +1 -0
- package/api/tests/libs/api.spec.ts +72 -1
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +19 -18
- package/src/components/customer/form.tsx +53 -0
- package/src/components/filter-toolbar.tsx +1 -1
- package/src/components/invoice/list.tsx +8 -8
- package/src/components/invoice/table.tsx +42 -36
- package/src/components/metadata/form.tsx +24 -3
- package/src/components/payment-intent/actions.tsx +17 -5
- package/src/components/payment-link/after-pay.tsx +46 -4
- package/src/components/payouts/list.tsx +1 -1
- package/src/components/price/form.tsx +14 -2
- package/src/components/pricing-table/payment-settings.tsx +45 -4
- package/src/components/product/features.tsx +16 -2
- package/src/components/product/form.tsx +28 -4
- package/src/components/subscription/actions/cancel.tsx +10 -0
- package/src/components/subscription/description.tsx +2 -2
- package/src/components/subscription/items/index.tsx +3 -2
- package/src/components/subscription/portal/cancel.tsx +12 -1
- package/src/components/subscription/portal/list.tsx +6 -5
- package/src/locales/en.tsx +6 -1
- package/src/locales/zh.tsx +6 -1
- package/src/pages/admin/billing/invoices/detail.tsx +17 -2
- package/src/pages/admin/billing/subscriptions/detail.tsx +4 -0
- package/src/pages/admin/customers/customers/detail.tsx +4 -0
- package/src/pages/admin/customers/customers/index.tsx +1 -1
- package/src/pages/admin/payments/intents/detail.tsx +4 -0
- package/src/pages/admin/payments/payouts/detail.tsx +4 -0
- package/src/pages/admin/payments/refunds/detail.tsx +4 -0
- package/src/pages/admin/products/links/detail.tsx +4 -0
- package/src/pages/admin/products/prices/detail.tsx +4 -0
- package/src/pages/admin/products/pricing-tables/detail.tsx +4 -0
- package/src/pages/admin/products/products/detail.tsx +4 -0
- package/src/pages/checkout/pricing-table.tsx +9 -3
- package/src/pages/customer/index.tsx +28 -17
- package/src/pages/customer/invoice/detail.tsx +27 -16
- package/src/pages/customer/invoice/past-due.tsx +3 -2
- package/src/pages/customer/subscription/detail.tsx +4 -0
|
@@ -4,7 +4,7 @@ import pick from 'lodash/pick';
|
|
|
4
4
|
import { Op } from 'sequelize';
|
|
5
5
|
import type { WhereOptions } from 'sequelize';
|
|
6
6
|
|
|
7
|
-
import { createListParamSchema } from '../libs/api';
|
|
7
|
+
import { createListParamSchema, MetadataSchema } from '../libs/api';
|
|
8
8
|
import logger from '../libs/logger';
|
|
9
9
|
import { authenticate } from '../libs/security';
|
|
10
10
|
import { isLineItemAligned } from '../libs/session';
|
|
@@ -141,9 +141,62 @@ export async function createPaymentLink(payload: any) {
|
|
|
141
141
|
return PaymentLink.create(raw as PaymentLink);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
const PaymentLinkCreateSchema = Joi.object({
|
|
145
|
+
name: Joi.string().required(),
|
|
146
|
+
line_items: Joi.array()
|
|
147
|
+
.items(
|
|
148
|
+
Joi.object({
|
|
149
|
+
price_id: Joi.string().required(),
|
|
150
|
+
quantity: Joi.number().min(1).required(),
|
|
151
|
+
adjustable_quantity: Joi.object({
|
|
152
|
+
enabled: Joi.boolean().required(),
|
|
153
|
+
minimum: Joi.number().min(0),
|
|
154
|
+
maximum: Joi.number().min(0),
|
|
155
|
+
})
|
|
156
|
+
.min(0)
|
|
157
|
+
.optional(),
|
|
158
|
+
})
|
|
159
|
+
)
|
|
160
|
+
.min(1)
|
|
161
|
+
.required(),
|
|
162
|
+
currency_id: Joi.string().optional(),
|
|
163
|
+
metadata: MetadataSchema,
|
|
164
|
+
after_completion: Joi.object({
|
|
165
|
+
type: Joi.string().valid('hosted_confirmation', 'redirect').required(),
|
|
166
|
+
hosted_confirmation: Joi.object({
|
|
167
|
+
custom_message: Joi.string().max(200).empty('').optional(),
|
|
168
|
+
})
|
|
169
|
+
.min(0)
|
|
170
|
+
.optional(),
|
|
171
|
+
redirect: Joi.object({
|
|
172
|
+
url: Joi.string().uri().max(2048).empty('').optional(),
|
|
173
|
+
})
|
|
174
|
+
.min(0)
|
|
175
|
+
.optional(),
|
|
176
|
+
})
|
|
177
|
+
.min(0)
|
|
178
|
+
.optional(),
|
|
179
|
+
allow_promotion_codes: Joi.boolean().optional(),
|
|
180
|
+
nft_mint_settings: Joi.object({
|
|
181
|
+
enabled: Joi.boolean().required(),
|
|
182
|
+
factory: Joi.string().max(40).empty('').optional(),
|
|
183
|
+
})
|
|
184
|
+
.min(0)
|
|
185
|
+
.optional(),
|
|
186
|
+
consent_collection: Joi.object({
|
|
187
|
+
promotions: Joi.string().valid('none', 'opt_in', 'opt_out').required(),
|
|
188
|
+
terms_of_service: Joi.string().valid('none', 'opt_in', 'opt_out').required(),
|
|
189
|
+
})
|
|
190
|
+
.min(0)
|
|
191
|
+
.optional(),
|
|
192
|
+
}).unknown(true);
|
|
145
193
|
router.post('/', auth, async (req, res) => {
|
|
146
194
|
try {
|
|
195
|
+
const { error } = PaymentLinkCreateSchema.validate(req.body);
|
|
196
|
+
if (error) {
|
|
197
|
+
res.status(400).json({ error: `Payment link create request invalid: ${error.message}` });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
147
200
|
const result = await createPaymentLink({
|
|
148
201
|
...req.body,
|
|
149
202
|
livemode: !!req.livemode,
|
|
@@ -218,9 +271,58 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
218
271
|
}
|
|
219
272
|
});
|
|
220
273
|
|
|
221
|
-
|
|
274
|
+
const PaymentLinkUpdateSchema = Joi.object({
|
|
275
|
+
name: Joi.string().optional(),
|
|
276
|
+
line_items: Joi.array()
|
|
277
|
+
.items(
|
|
278
|
+
Joi.object({
|
|
279
|
+
price_id: Joi.string().required(),
|
|
280
|
+
quantity: Joi.number().min(1).required(),
|
|
281
|
+
adjustable_quantity: Joi.object({
|
|
282
|
+
enabled: Joi.boolean().required(),
|
|
283
|
+
minimum: Joi.number().min(0),
|
|
284
|
+
maximum: Joi.number().min(0),
|
|
285
|
+
})
|
|
286
|
+
.min(0)
|
|
287
|
+
.optional(),
|
|
288
|
+
})
|
|
289
|
+
)
|
|
290
|
+
.min(1)
|
|
291
|
+
.optional(),
|
|
292
|
+
currency_id: Joi.string().optional(),
|
|
293
|
+
metadata: MetadataSchema,
|
|
294
|
+
active: Joi.boolean().optional(),
|
|
295
|
+
after_completion: Joi.object({
|
|
296
|
+
type: Joi.string().valid('hosted_confirmation', 'redirect').optional(),
|
|
297
|
+
hosted_confirmation: Joi.object({
|
|
298
|
+
custom_message: Joi.string().max(200).empty('').optional(),
|
|
299
|
+
}),
|
|
300
|
+
redirect: Joi.object({
|
|
301
|
+
url: Joi.string().uri().max(2048).empty('').optional(),
|
|
302
|
+
}),
|
|
303
|
+
})
|
|
304
|
+
.min(0)
|
|
305
|
+
.optional(),
|
|
306
|
+
allow_promotion_codes: Joi.boolean().optional(),
|
|
307
|
+
nft_mint_settings: Joi.object({
|
|
308
|
+
enabled: Joi.boolean().required(),
|
|
309
|
+
factory: Joi.string().max(40).empty('').optional(),
|
|
310
|
+
})
|
|
311
|
+
.min(0)
|
|
312
|
+
.optional(),
|
|
313
|
+
consent_collection: Joi.object({
|
|
314
|
+
promotions: Joi.string().valid('none', 'opt_in', 'opt_out').optional(),
|
|
315
|
+
terms_of_service: Joi.string().valid('none', 'opt_in', 'opt_out').optional(),
|
|
316
|
+
})
|
|
317
|
+
.min(0)
|
|
318
|
+
.optional(),
|
|
319
|
+
}).unknown(true);
|
|
222
320
|
// eslint-disable-next-line consistent-return
|
|
223
321
|
router.put('/:id', auth, async (req, res) => {
|
|
322
|
+
const { error } = PaymentLinkUpdateSchema.validate(req.body);
|
|
323
|
+
if (error) {
|
|
324
|
+
return res.status(400).json({ error: `Payment link update request invalid: ${error.message}` });
|
|
325
|
+
}
|
|
224
326
|
const doc = await PaymentLink.findByPk(req.params.id);
|
|
225
327
|
|
|
226
328
|
if (!doc) {
|
|
@@ -3,7 +3,7 @@ import { Router } from 'express';
|
|
|
3
3
|
import Joi from 'joi';
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
5
|
|
|
6
|
-
import { createListParamSchema, getWhereFromKvQuery } from '../libs/api';
|
|
6
|
+
import { createListParamSchema, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
|
|
7
7
|
import { authenticate } from '../libs/security';
|
|
8
8
|
import { formatMetadata } from '../libs/util';
|
|
9
9
|
import { Customer } from '../store/models/customer';
|
|
@@ -137,6 +137,10 @@ router.put('/:id', authAdmin, async (req, res) => {
|
|
|
137
137
|
|
|
138
138
|
const raw = pick(req.body, ['metadata']);
|
|
139
139
|
if (raw.metadata) {
|
|
140
|
+
const { error: metadataError } = MetadataSchema.validate(raw.metadata);
|
|
141
|
+
if (metadataError) {
|
|
142
|
+
return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
|
|
143
|
+
}
|
|
140
144
|
raw.metadata = formatMetadata(raw.metadata);
|
|
141
145
|
}
|
|
142
146
|
|
package/api/src/routes/prices.ts
CHANGED
|
@@ -4,7 +4,7 @@ import Joi from 'joi';
|
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
5
|
import type { WhereOptions } from 'sequelize';
|
|
6
6
|
|
|
7
|
-
import { createListParamSchema, getWhereFromQuery } from '../libs/api';
|
|
7
|
+
import { createListParamSchema, getWhereFromQuery, MetadataSchema } from '../libs/api';
|
|
8
8
|
import logger from '../libs/logger';
|
|
9
9
|
import { authenticate } from '../libs/security';
|
|
10
10
|
import { canUpsell } from '../libs/session';
|
|
@@ -182,6 +182,8 @@ const priceAmountSchema = Joi.object({
|
|
|
182
182
|
)
|
|
183
183
|
.optional(),
|
|
184
184
|
unit_amount: Joi.number().greater(0).optional(),
|
|
185
|
+
nickname: Joi.string().max(64).empty('').optional(),
|
|
186
|
+
lookup_key: Joi.string().max(64).empty('').optional(),
|
|
185
187
|
});
|
|
186
188
|
|
|
187
189
|
// FIXME: @wangshijun use schema validation
|
|
@@ -195,10 +197,18 @@ router.post('/', auth, async (req, res) => {
|
|
|
195
197
|
if (error) {
|
|
196
198
|
return res.status(400).json({ error: `Price create request invalid: ${error.message}` });
|
|
197
199
|
}
|
|
198
|
-
const { error: priceAmountError } = priceAmountSchema.validate(
|
|
200
|
+
const { error: priceAmountError } = priceAmountSchema.validate(
|
|
201
|
+
pick(req.body, ['currency_options', 'unit_amount', 'nickname', 'lookup_key'])
|
|
202
|
+
);
|
|
199
203
|
if (priceAmountError) {
|
|
200
204
|
return res.status(400).json({ error: `Price create request invalid: ${priceAmountError.message}` });
|
|
201
205
|
}
|
|
206
|
+
if (req.body.metadata) {
|
|
207
|
+
const { error: metadataError } = MetadataSchema.validate(req.body.metadata);
|
|
208
|
+
if (metadataError) {
|
|
209
|
+
return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
202
212
|
const result = await createPrice({
|
|
203
213
|
...req.body,
|
|
204
214
|
livemode: !!req.livemode,
|
|
@@ -265,7 +275,9 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
265
275
|
return res.status(400).json({ error: `Price update request invalid: ${error.message}` });
|
|
266
276
|
}
|
|
267
277
|
|
|
268
|
-
const { error: priceAmountError } = priceAmountSchema.validate(
|
|
278
|
+
const { error: priceAmountError } = priceAmountSchema.validate(
|
|
279
|
+
pick(req.body, ['currency_options', 'unit_amount', 'nickname', 'lookup_key'])
|
|
280
|
+
);
|
|
269
281
|
if (priceAmountError) {
|
|
270
282
|
return res.status(400).json({ error: `Price update request invalid: ${priceAmountError.message}` });
|
|
271
283
|
}
|
|
@@ -285,6 +297,10 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
285
297
|
}
|
|
286
298
|
|
|
287
299
|
const locked = doc.locked && process.env.PAYMENT_CHANGE_LOCKED_PRICE !== '1';
|
|
300
|
+
const { error: metadataError } = MetadataSchema.validate(req.body.metadata);
|
|
301
|
+
if (metadataError) {
|
|
302
|
+
return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
|
|
303
|
+
}
|
|
288
304
|
const updates: Partial<Price> = Price.formatBeforeSave(
|
|
289
305
|
pick(
|
|
290
306
|
req.body,
|
|
@@ -8,7 +8,7 @@ import uniq from 'lodash/uniq';
|
|
|
8
8
|
import type { WhereOptions } from 'sequelize';
|
|
9
9
|
|
|
10
10
|
import { checkPassportForPricingTable } from '../integrations/blocklet/passport';
|
|
11
|
-
import { createListParamSchema } from '../libs/api';
|
|
11
|
+
import { createListParamSchema, MetadataSchema } from '../libs/api';
|
|
12
12
|
import logger from '../libs/logger';
|
|
13
13
|
import { authenticate } from '../libs/security';
|
|
14
14
|
import { getBillingThreshold, getMinStakeAmount, isLineItemCurrencyAligned } from '../libs/session';
|
|
@@ -24,9 +24,46 @@ import { formatCheckoutSession } from './checkout-sessions';
|
|
|
24
24
|
const router = Router();
|
|
25
25
|
const auth = authenticate<PricingTable>({ component: true, roles: ['owner', 'admin'] });
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
const PricingTableCreateSchema = Joi.object({
|
|
28
|
+
name: Joi.string().required(),
|
|
29
|
+
description: Joi.string().allow(null, ''),
|
|
30
|
+
metadata: MetadataSchema,
|
|
31
|
+
items: Joi.array()
|
|
32
|
+
.items(
|
|
33
|
+
Joi.object({
|
|
34
|
+
price_id: Joi.string().required(),
|
|
35
|
+
product_id: Joi.string().required(),
|
|
36
|
+
after_completion: Joi.object({
|
|
37
|
+
type: Joi.string().valid('hosted_confirmation', 'redirect').required(),
|
|
38
|
+
hosted_confirmation: Joi.object({
|
|
39
|
+
custom_message: Joi.string().max(200).empty('').optional(),
|
|
40
|
+
})
|
|
41
|
+
.min(0)
|
|
42
|
+
.optional(),
|
|
43
|
+
redirect: Joi.object({
|
|
44
|
+
url: Joi.string().uri().max(2048).empty('').optional(),
|
|
45
|
+
})
|
|
46
|
+
.min(0)
|
|
47
|
+
.optional(),
|
|
48
|
+
}).unknown(true),
|
|
49
|
+
nft_mint_settings: Joi.object({
|
|
50
|
+
enabled: Joi.boolean().required(),
|
|
51
|
+
factory: Joi.string().max(40).empty('').optional(),
|
|
52
|
+
})
|
|
53
|
+
.min(0)
|
|
54
|
+
.optional(),
|
|
55
|
+
}).unknown(true)
|
|
56
|
+
)
|
|
57
|
+
.min(1)
|
|
58
|
+
.required(),
|
|
59
|
+
branding_settings: Joi.object().allow(null, {}).optional(),
|
|
60
|
+
}).unknown(true);
|
|
28
61
|
// eslint-disable-next-line consistent-return
|
|
29
62
|
router.post('/', auth, async (req, res) => {
|
|
63
|
+
const { error } = PricingTableCreateSchema.validate(req.body);
|
|
64
|
+
if (error) {
|
|
65
|
+
return res.status(400).json({ error: `Pricing table create request invalid: ${error.message}` });
|
|
66
|
+
}
|
|
30
67
|
const raw: Partial<PricingTable> = PricingTable.format(req.body);
|
|
31
68
|
raw.active = true;
|
|
32
69
|
raw.locked = false;
|
|
@@ -110,8 +147,48 @@ router.get('/:id', async (req, res) => {
|
|
|
110
147
|
});
|
|
111
148
|
|
|
112
149
|
// update
|
|
150
|
+
// 更新schema,局部更新
|
|
151
|
+
const PricingTableUpdateSchema = Joi.object({
|
|
152
|
+
name: Joi.string().optional(),
|
|
153
|
+
description: Joi.string().allow(null, '').optional(),
|
|
154
|
+
metadata: MetadataSchema.optional(),
|
|
155
|
+
items: Joi.array()
|
|
156
|
+
.items(
|
|
157
|
+
Joi.object({
|
|
158
|
+
price_id: Joi.string().required(),
|
|
159
|
+
product_id: Joi.string().required(),
|
|
160
|
+
after_completion: Joi.object({
|
|
161
|
+
type: Joi.string().valid('hosted_confirmation', 'redirect').required(),
|
|
162
|
+
hosted_confirmation: Joi.object({
|
|
163
|
+
custom_message: Joi.string().max(200).empty('').optional(),
|
|
164
|
+
})
|
|
165
|
+
.min(0)
|
|
166
|
+
.optional(),
|
|
167
|
+
redirect: Joi.object({
|
|
168
|
+
url: Joi.string().uri().max(2048).empty('').optional(),
|
|
169
|
+
})
|
|
170
|
+
.min(0)
|
|
171
|
+
.optional(),
|
|
172
|
+
}).unknown(true),
|
|
173
|
+
nft_mint_settings: Joi.object({
|
|
174
|
+
enabled: Joi.boolean().required(),
|
|
175
|
+
factory: Joi.string().max(40).empty('').optional(),
|
|
176
|
+
})
|
|
177
|
+
.min(0)
|
|
178
|
+
.optional(),
|
|
179
|
+
}).unknown(true)
|
|
180
|
+
)
|
|
181
|
+
.min(1)
|
|
182
|
+
.optional(),
|
|
183
|
+
branding_settings: Joi.object().allow(null, {}).optional(),
|
|
184
|
+
}).unknown(true);
|
|
185
|
+
|
|
113
186
|
// eslint-disable-next-line consistent-return
|
|
114
187
|
router.put('/:id', auth, async (req, res) => {
|
|
188
|
+
const { error } = PricingTableUpdateSchema.validate(req.body);
|
|
189
|
+
if (error) {
|
|
190
|
+
return res.status(400).json({ error: `Pricing table create request invalid: ${error.message}` });
|
|
191
|
+
}
|
|
115
192
|
const doc = await PricingTable.findByPk(req.params.id);
|
|
116
193
|
|
|
117
194
|
if (!doc) {
|
|
@@ -5,7 +5,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
|
|
5
5
|
import pick from 'lodash/pick';
|
|
6
6
|
import { Op } from 'sequelize';
|
|
7
7
|
|
|
8
|
-
import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
|
|
8
|
+
import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
|
|
9
9
|
import logger from '../libs/logger';
|
|
10
10
|
import { authenticate } from '../libs/security';
|
|
11
11
|
import { formatMetadata } from '../libs/util';
|
|
@@ -18,7 +18,18 @@ const router = Router();
|
|
|
18
18
|
|
|
19
19
|
const auth = authenticate<Product>({ component: true, roles: ['owner', 'admin'] });
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const ProductAndPriceSchema = Joi.object({
|
|
22
|
+
name: Joi.string().max(64).empty('').optional(),
|
|
23
|
+
type: Joi.string().valid('service', 'good').empty('').optional(),
|
|
24
|
+
description: Joi.string().max(256).empty('').optional(),
|
|
25
|
+
images: Joi.any().optional(),
|
|
26
|
+
metadata: MetadataSchema,
|
|
27
|
+
statement_descriptor: Joi.string().max(32).empty('').optional(),
|
|
28
|
+
unit_label: Joi.string().max(32).empty('').optional(),
|
|
29
|
+
nft_factory: Joi.string().max(40).allow(null).empty('').optional(),
|
|
30
|
+
features: Joi.array()
|
|
31
|
+
.items(Joi.object({ name: Joi.string().max(64).empty('').optional() }).unknown(true))
|
|
32
|
+
.optional(),
|
|
22
33
|
prices: Joi.array()
|
|
23
34
|
.items(
|
|
24
35
|
Joi.object({
|
|
@@ -26,15 +37,16 @@ const priceAmountSchema = Joi.object({
|
|
|
26
37
|
.items(
|
|
27
38
|
Joi.object({
|
|
28
39
|
unit_amount: Joi.number().greater(0).required(),
|
|
29
|
-
// 其他属性
|
|
30
40
|
}).unknown(true)
|
|
31
41
|
)
|
|
32
42
|
.optional(),
|
|
33
43
|
unit_amount: Joi.number().greater(0).required(),
|
|
44
|
+
nickname: Joi.string().max(64).empty('').optional(),
|
|
45
|
+
lookup_key: Joi.string().max(64).empty('').optional(),
|
|
34
46
|
}).unknown(true)
|
|
35
47
|
)
|
|
36
|
-
.
|
|
37
|
-
});
|
|
48
|
+
.optional(),
|
|
49
|
+
}).unknown(true);
|
|
38
50
|
|
|
39
51
|
export async function createProductAndPrices(payload: any) {
|
|
40
52
|
const raw: Partial<Product> = pick(payload, [
|
|
@@ -128,9 +140,9 @@ export async function createProductAndPrices(payload: any) {
|
|
|
128
140
|
// eslint-disable-next-line consistent-return
|
|
129
141
|
router.post('/', auth, async (req, res) => {
|
|
130
142
|
try {
|
|
131
|
-
const { error
|
|
132
|
-
if (
|
|
133
|
-
return res.status(400).json({ error: `Product create request invalid: ${
|
|
143
|
+
const { error } = ProductAndPriceSchema.validate(req.body);
|
|
144
|
+
if (error) {
|
|
145
|
+
return res.status(400).json({ error: `Product create request invalid: ${error.message}` });
|
|
134
146
|
}
|
|
135
147
|
const result = await createProductAndPrices({
|
|
136
148
|
...req.body,
|
|
@@ -272,6 +284,10 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
272
284
|
'metadata',
|
|
273
285
|
'cross_sell',
|
|
274
286
|
]);
|
|
287
|
+
const { error } = ProductAndPriceSchema.validate(updates);
|
|
288
|
+
if (error) {
|
|
289
|
+
return res.status(400).json({ error: `Product update request invalid: ${error.message}` });
|
|
290
|
+
}
|
|
275
291
|
if (updates.metadata) {
|
|
276
292
|
updates.metadata = formatMetadata(updates.metadata);
|
|
277
293
|
}
|
|
@@ -5,7 +5,7 @@ import Joi from 'joi';
|
|
|
5
5
|
import pick from 'lodash/pick';
|
|
6
6
|
|
|
7
7
|
import { BN, fromTokenToUnit } from '@ocap/util';
|
|
8
|
-
import { BNPositiveValidator, createListParamSchema, getWhereFromKvQuery } from '../libs/api';
|
|
8
|
+
import { BNPositiveValidator, createListParamSchema, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
|
|
9
9
|
import { authenticate } from '../libs/security';
|
|
10
10
|
import { formatMetadata } from '../libs/util';
|
|
11
11
|
import {
|
|
@@ -121,8 +121,8 @@ const refundRequestSchema = Joi.object({
|
|
|
121
121
|
reason: Joi.string()
|
|
122
122
|
.valid('duplicate', 'requested_by_customer', 'requested_by_admin', 'fraudulent', 'expired_uncaptured_charge')
|
|
123
123
|
.required(),
|
|
124
|
-
description: Joi.string().required(),
|
|
125
|
-
metadata:
|
|
124
|
+
description: Joi.string().max(200).required(),
|
|
125
|
+
metadata: MetadataSchema,
|
|
126
126
|
invoice_id: Joi.string().optional(),
|
|
127
127
|
subscription_id: Joi.string().optional(),
|
|
128
128
|
});
|
|
@@ -149,7 +149,6 @@ router.post('/', authAdmin, async (req, res) => {
|
|
|
149
149
|
if (!paymentMethod) {
|
|
150
150
|
throw new Error(`payment method not found: ${req.body.payment_method_id}`);
|
|
151
151
|
}
|
|
152
|
-
|
|
153
152
|
const item = await Refund.create({
|
|
154
153
|
...req.body,
|
|
155
154
|
type: 'refund',
|
|
@@ -234,6 +233,10 @@ router.put('/:id', authAdmin, async (req, res) => {
|
|
|
234
233
|
|
|
235
234
|
const raw = pick(req.body, ['metadata']);
|
|
236
235
|
if (raw.metadata) {
|
|
236
|
+
const { error: metadataError } = MetadataSchema.validate(raw.metadata);
|
|
237
|
+
if (metadataError) {
|
|
238
|
+
return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
|
|
239
|
+
}
|
|
237
240
|
raw.metadata = formatMetadata(raw.metadata);
|
|
238
241
|
}
|
|
239
242
|
|
|
@@ -4,7 +4,7 @@ import Joi from 'joi';
|
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
5
|
import type { WhereOptions } from 'sequelize';
|
|
6
6
|
|
|
7
|
-
import { createListParamSchema } from '../libs/api';
|
|
7
|
+
import { createListParamSchema, MetadataSchema } from '../libs/api';
|
|
8
8
|
import { authenticate } from '../libs/security';
|
|
9
9
|
import { expandLineItems } from '../libs/session';
|
|
10
10
|
import { formatMetadata } from '../libs/util';
|
|
@@ -32,6 +32,10 @@ router.post('/', auth, async (req, res) => {
|
|
|
32
32
|
|
|
33
33
|
raw.livemode = req.livemode;
|
|
34
34
|
if (raw.metadata) {
|
|
35
|
+
const { error: metadataError } = MetadataSchema.validate(raw.metadata);
|
|
36
|
+
if (metadataError) {
|
|
37
|
+
return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
|
|
38
|
+
}
|
|
35
39
|
raw.metadata = formatMetadata(raw.metadata);
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -9,7 +9,7 @@ import uniq from 'lodash/uniq';
|
|
|
9
9
|
import { literal } from 'sequelize';
|
|
10
10
|
import type { Literal } from 'sequelize/types/utils';
|
|
11
11
|
import { ensureStripeCustomer, ensureStripePrice, ensureStripeSubscription } from '../integrations/stripe/resource';
|
|
12
|
-
import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
|
|
12
|
+
import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
|
|
13
13
|
import dayjs from '../libs/dayjs';
|
|
14
14
|
import logger from '../libs/logger';
|
|
15
15
|
import { isDelegationSufficientForPayment } from '../libs/payment';
|
|
@@ -121,7 +121,7 @@ router.get('/', authMine, async (req, res) => {
|
|
|
121
121
|
|
|
122
122
|
if (query.activeFirst) {
|
|
123
123
|
order.unshift([
|
|
124
|
-
literal("CASE status WHEN 'active' THEN 1 WHEN 'trialing' THEN
|
|
124
|
+
literal("CASE status WHEN 'active' THEN 1 WHEN 'trialing' THEN 1 WHEN 'past_due' THEN 2 ELSE 3 END"),
|
|
125
125
|
'ASC',
|
|
126
126
|
]);
|
|
127
127
|
}
|
|
@@ -221,7 +221,14 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
221
221
|
}
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
+
const CommentSchema = Joi.string().max(200).empty('').optional();
|
|
225
|
+
|
|
224
226
|
router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
227
|
+
const { error: commentError } = CommentSchema.validate(req.body?.comment);
|
|
228
|
+
if (commentError) {
|
|
229
|
+
return res.status(400).json({ error: `comment invalid: ${commentError.message}` });
|
|
230
|
+
}
|
|
231
|
+
|
|
225
232
|
const subscription = await Subscription.findByPk(req.params.id);
|
|
226
233
|
logger.info('subscription cancel request', { ...req.params, ...req.body });
|
|
227
234
|
|
|
@@ -250,6 +257,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
250
257
|
if ((requestByAdmin && staking === 'proration') || req.body?.cancel_from === 'customer') {
|
|
251
258
|
canReturnStake = true;
|
|
252
259
|
}
|
|
260
|
+
const slashStake = requestByAdmin && staking === 'slash';
|
|
253
261
|
// update cancel at
|
|
254
262
|
const updates: Partial<Subscription> = {
|
|
255
263
|
cancelation_details: {
|
|
@@ -257,14 +265,26 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
257
265
|
reason: reason || 'payment_disputed',
|
|
258
266
|
feedback: feedback || 'other',
|
|
259
267
|
return_stake: canReturnStake,
|
|
268
|
+
slash_stake: slashStake,
|
|
260
269
|
},
|
|
261
270
|
};
|
|
262
271
|
const now = dayjs().unix() + 3;
|
|
263
|
-
if (req.user?.via === 'portal') {
|
|
272
|
+
if (req.user?.via === 'portal' || req.body?.cancel_from === 'customer') {
|
|
273
|
+
const inTrialing = subscription.status === 'trialing';
|
|
264
274
|
updates.cancel_at_period_end = true;
|
|
265
275
|
updates.cancel_at = subscription.current_period_end;
|
|
266
|
-
updates.cancelation_details = {
|
|
276
|
+
updates.cancelation_details = {
|
|
277
|
+
reason: 'cancellation_requested',
|
|
278
|
+
feedback,
|
|
279
|
+
comment,
|
|
280
|
+
return_stake: canReturnStake,
|
|
281
|
+
slash_stake: slashStake,
|
|
282
|
+
};
|
|
267
283
|
updates.canceled_at = now;
|
|
284
|
+
if (inTrialing) {
|
|
285
|
+
updates.cancel_at_period_end = false;
|
|
286
|
+
updates.cancel_at = now;
|
|
287
|
+
}
|
|
268
288
|
await addSubscriptionJob(subscription, 'cancel', true, updates.cancel_at);
|
|
269
289
|
} else {
|
|
270
290
|
if (['owner', 'admin'].includes(req.user?.role as string) === false) {
|
|
@@ -614,7 +634,7 @@ const updateSchema = Joi.object<{
|
|
|
614
634
|
service_actions?: ServiceAction[];
|
|
615
635
|
}>({
|
|
616
636
|
description: Joi.string().min(1).optional(),
|
|
617
|
-
metadata:
|
|
637
|
+
metadata: MetadataSchema,
|
|
618
638
|
payment_behavior: Joi.string().allow('allow_incomplete', 'error_if_incomplete', 'pending_if_incomplete').optional(),
|
|
619
639
|
proration_behavior: Joi.string().allow('always_invoice', 'create_prorations', 'none').optional(),
|
|
620
640
|
billing_cycle_anchor: Joi.string().allow('now', 'unchanged').optional(),
|
|
@@ -3,7 +3,7 @@ import Joi from 'joi';
|
|
|
3
3
|
import pick from 'lodash/pick';
|
|
4
4
|
import type { WhereOptions } from 'sequelize';
|
|
5
5
|
|
|
6
|
-
import { createListParamSchema } from '../libs/api';
|
|
6
|
+
import { createListParamSchema, MetadataSchema } from '../libs/api';
|
|
7
7
|
import { authenticate } from '../libs/security';
|
|
8
8
|
import { formatMetadata } from '../libs/util';
|
|
9
9
|
import { WebhookEndpoint } from '../store/models';
|
|
@@ -22,6 +22,10 @@ router.post('/', auth, async (req, res) => {
|
|
|
22
22
|
raw.api_version = '2023-09-05';
|
|
23
23
|
raw.status = raw.status || 'enabled';
|
|
24
24
|
if (raw.metadata) {
|
|
25
|
+
const { error } = MetadataSchema.validate(raw.metadata);
|
|
26
|
+
if (error) {
|
|
27
|
+
return res.status(400).json({ error: `metadata invalid: ${error.message}` });
|
|
28
|
+
}
|
|
25
29
|
raw.metadata = formatMetadata(raw.metadata);
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -62,6 +62,7 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
|
|
|
62
62
|
>;
|
|
63
63
|
reason: LiteralUnion<'cancellation_requested' | 'payment_disputed' | 'payment_failed' | 'stake_revoked', string>;
|
|
64
64
|
return_stake?: boolean;
|
|
65
|
+
slash_stake?: boolean;
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
declare billing_cycle_anchor: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Op } from 'sequelize';
|
|
2
2
|
|
|
3
|
-
import { getWhereFromQuery } from '../../src/libs/api';
|
|
3
|
+
import { getWhereFromQuery, MetadataSchema } from '../../src/libs/api';
|
|
4
4
|
|
|
5
5
|
describe('getWhereFromQuery', () => {
|
|
6
6
|
it('should correctly parse > operator', () => {
|
|
@@ -155,3 +155,74 @@ describe('getWhereFromQuery', () => {
|
|
|
155
155
|
expect(() => getWhereFromQuery('status === "failed"')).toThrow(/Operator not supported/);
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
|
+
|
|
159
|
+
describe('MetadataSchema', () => {
|
|
160
|
+
it('should validate an object with string keys and custom validated values', () => {
|
|
161
|
+
const data = {
|
|
162
|
+
key1: 'value1',
|
|
163
|
+
key2: 123,
|
|
164
|
+
};
|
|
165
|
+
const { error } = MetadataSchema.validate(data);
|
|
166
|
+
expect(error).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should validate an array of objects with key and custom validated value', () => {
|
|
170
|
+
const data = [
|
|
171
|
+
{ key: 'key1', value: 'value1' },
|
|
172
|
+
{ key: 'key2', value: 123 },
|
|
173
|
+
];
|
|
174
|
+
const { error } = MetadataSchema.validate(data);
|
|
175
|
+
expect(error).toBeUndefined();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should invalidate an object with a key longer than 64 characters', () => {
|
|
179
|
+
const data = {
|
|
180
|
+
['a'.repeat(65)]: 'value1',
|
|
181
|
+
};
|
|
182
|
+
const { error } = MetadataSchema.validate(data);
|
|
183
|
+
expect(error).toBeDefined();
|
|
184
|
+
expect(error?.details?.[0]?.message).toMatch(
|
|
185
|
+
/"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is not allowed/
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should invalidate an array with an object missing the key field', () => {
|
|
190
|
+
const data = [{ value: 'value1' }];
|
|
191
|
+
const { error } = MetadataSchema.validate(data);
|
|
192
|
+
expect(error).toBeDefined();
|
|
193
|
+
expect(error?.details?.[0]?.message).toMatch(/"\[0\]\.key" is required/);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should invalidate an array with an object missing the value field', () => {
|
|
197
|
+
const data = [{ key: 'key1' }];
|
|
198
|
+
const { error } = MetadataSchema.validate(data);
|
|
199
|
+
expect(error).toBeDefined();
|
|
200
|
+
expect(error?.details?.[0]?.message).toMatch(/"\[0\]\.value" is required/);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should invalidate an object with a value longer than 256 characters', () => {
|
|
204
|
+
const data = {
|
|
205
|
+
key1: 'a'.repeat(257),
|
|
206
|
+
};
|
|
207
|
+
const { error } = MetadataSchema.validate(data);
|
|
208
|
+
expect(error).toBeDefined();
|
|
209
|
+
expect(error?.details?.[0]?.message).toMatch(/Metadata value should be less than 256 characters/);
|
|
210
|
+
});
|
|
211
|
+
it('should validate when data is null', () => {
|
|
212
|
+
const data = null;
|
|
213
|
+
const { error } = MetadataSchema.validate(data);
|
|
214
|
+
expect(error).toBeUndefined();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should validate when data is empty object', () => {
|
|
218
|
+
const data = {};
|
|
219
|
+
const { error } = MetadataSchema.validate(data);
|
|
220
|
+
expect(error).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should validate when data is empty array', () => {
|
|
224
|
+
const data: any[] = [];
|
|
225
|
+
const { error } = MetadataSchema.validate(data);
|
|
226
|
+
expect(error).toBeUndefined();
|
|
227
|
+
});
|
|
228
|
+
});
|
package/api/third.d.ts
CHANGED