payment-kit 1.13.81 → 1.13.83
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 +3 -2
- package/api/src/libs/api.ts +72 -0
- package/api/src/routes/checkout-sessions.ts +5 -2
- package/api/src/routes/customers.ts +34 -0
- package/api/src/routes/payment-intents.ts +39 -1
- package/api/src/routes/prices.ts +93 -59
- package/api/src/routes/products.ts +34 -0
- package/api/src/routes/subscriptions.ts +52 -10
- package/api/src/store/models/types.ts +5 -1
- package/api/tests/libs/api.spec.ts +157 -0
- package/api/tests/libs/session.spec.ts +135 -0
- package/{tests/api → api/tests}/libs/subscription.spec.ts +1 -1
- package/api/tests/libs/util.spec.ts +177 -0
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/jest.config.js +3 -1
- package/package.json +9 -8
- package/scripts/jest.js +1 -0
package/api/src/index.ts
CHANGED
|
@@ -55,9 +55,8 @@ handlers.attach(Object.assign({ app: router }, setupHandlers));
|
|
|
55
55
|
handlers.attach(Object.assign({ app: router }, subscribeHandlers));
|
|
56
56
|
|
|
57
57
|
router.use('/api', routes);
|
|
58
|
-
app.use(router);
|
|
59
58
|
|
|
60
|
-
const isProduction = process.env.
|
|
59
|
+
const isProduction = process.env.BLOCKLET_MODE === 'production';
|
|
61
60
|
|
|
62
61
|
const accessFormat =
|
|
63
62
|
isProduction
|
|
@@ -76,6 +75,8 @@ if (isProduction) {
|
|
|
76
75
|
});
|
|
77
76
|
}
|
|
78
77
|
|
|
78
|
+
app.use(router);
|
|
79
|
+
|
|
79
80
|
if (isProduction) {
|
|
80
81
|
const staticDir = path.resolve(process.env.BLOCKLET_APP_DIR!, 'dist');
|
|
81
82
|
app.use(express.static(staticDir, { maxAge: '30d', index: false }));
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Op } from 'sequelize';
|
|
2
|
+
import SqlWhereParser from 'sql-where-parser';
|
|
3
|
+
|
|
4
|
+
const parser = new SqlWhereParser();
|
|
5
|
+
|
|
6
|
+
export function handleOperator(operator: string, operands: any[]): any {
|
|
7
|
+
switch (operator) {
|
|
8
|
+
case 'AND':
|
|
9
|
+
case 'OR':
|
|
10
|
+
return handleCondition({ [operator]: operands });
|
|
11
|
+
case 'IN':
|
|
12
|
+
if (operands[0].NOT) {
|
|
13
|
+
return { [operands[0].NOT]: { [Op.notIn]: operands[1] } };
|
|
14
|
+
}
|
|
15
|
+
return { [operands[0]]: { [Op.in]: operands[1] } };
|
|
16
|
+
case 'LIKE':
|
|
17
|
+
if (operands[0].NOT) {
|
|
18
|
+
return { [operands[0].NOT]: { [Op.notLike]: operands[1] } };
|
|
19
|
+
}
|
|
20
|
+
return { [operands[0]]: { [Op.like]: operands[1] } };
|
|
21
|
+
case 'BETWEEN':
|
|
22
|
+
if (operands[0].NOT) {
|
|
23
|
+
return { [operands[0].NOT]: { [Op.notBetween]: [operands[1], operands[2]] } };
|
|
24
|
+
}
|
|
25
|
+
return { [operands[0]]: { [Op.between]: [operands[1], operands[2]] } };
|
|
26
|
+
case 'IS':
|
|
27
|
+
if (operands[1] && operands[1].NOT && Array.isArray(operands[1].NOT) && operands[1].NOT.length) {
|
|
28
|
+
return { [operands[0]]: { [Op.ne]: operands[1].NOT[0] } };
|
|
29
|
+
}
|
|
30
|
+
return { [operands[0]]: operands[1] };
|
|
31
|
+
case '=':
|
|
32
|
+
return { [operands[0]]: operands[1] };
|
|
33
|
+
case '!=':
|
|
34
|
+
return { [operands[0]]: { [Op.ne]: operands[1] } };
|
|
35
|
+
case '>':
|
|
36
|
+
return { [operands[0]]: { [Op.gt]: operands[1] } };
|
|
37
|
+
case '>=':
|
|
38
|
+
return { [operands[0]]: { [Op.gte]: operands[1] } };
|
|
39
|
+
case '<':
|
|
40
|
+
return { [operands[0]]: { [Op.lt]: operands[1] } };
|
|
41
|
+
case '<=':
|
|
42
|
+
return { [operands[0]]: { [Op.lte]: operands[1] } };
|
|
43
|
+
default:
|
|
44
|
+
console.error('Invalid operator', { operator, operands });
|
|
45
|
+
throw new Error('Operator not supported when parse query');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function handleItem(item: Record<string, any>) {
|
|
50
|
+
const operator = Object.keys(item)[0] as string;
|
|
51
|
+
const operands = item[operator];
|
|
52
|
+
return handleOperator(operator, operands);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function handleCondition(condition: any) {
|
|
56
|
+
if (condition.AND) {
|
|
57
|
+
return { [Op.and]: condition.AND.map(handleItem) };
|
|
58
|
+
}
|
|
59
|
+
if (condition.OR) {
|
|
60
|
+
return { [Op.or]: condition.OR.map(handleItem) };
|
|
61
|
+
}
|
|
62
|
+
return handleItem(condition);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getWhereFromQuery(query: string) {
|
|
66
|
+
if (!query) {
|
|
67
|
+
throw new Error('Can not parse empty query');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const parsed = parser.parse(query);
|
|
71
|
+
return handleCondition(parsed);
|
|
72
|
+
}
|
|
@@ -1092,8 +1092,11 @@ router.get('/', auth, async (req, res) => {
|
|
|
1092
1092
|
const prices = (await Price.findAll(condition)).map((x) => x.toJSON());
|
|
1093
1093
|
const docs = list.map((x) => x.toJSON());
|
|
1094
1094
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1095
|
+
docs.forEach((x) => {
|
|
1096
|
+
// @ts-ignore
|
|
1097
|
+
expandLineItems(x.line_items, products, prices);
|
|
1098
|
+
x.url = getUrl(`/checkout/${x.submit_type}/${x.id}`);
|
|
1099
|
+
});
|
|
1097
1100
|
|
|
1098
1101
|
res.json({ count, list: docs });
|
|
1099
1102
|
} catch (err) {
|
|
@@ -4,6 +4,7 @@ import Joi from 'joi';
|
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
5
|
import type { WhereOptions } from 'sequelize';
|
|
6
6
|
|
|
7
|
+
import { getWhereFromQuery } from '../libs/api';
|
|
7
8
|
import { authenticate } from '../libs/security';
|
|
8
9
|
import { formatMetadata } from '../libs/util';
|
|
9
10
|
import { Customer } from '../store/models/customer';
|
|
@@ -53,6 +54,39 @@ router.get('/', auth, async (req, res) => {
|
|
|
53
54
|
}
|
|
54
55
|
});
|
|
55
56
|
|
|
57
|
+
// search customers
|
|
58
|
+
const searchSchema = Joi.object<{
|
|
59
|
+
page: number;
|
|
60
|
+
pageSize: number;
|
|
61
|
+
query: string;
|
|
62
|
+
livemode?: boolean;
|
|
63
|
+
}>({
|
|
64
|
+
page: Joi.number().integer().min(1).default(1),
|
|
65
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
66
|
+
query: Joi.string(),
|
|
67
|
+
livemode: Joi.boolean().empty(''),
|
|
68
|
+
});
|
|
69
|
+
router.get('/search', auth, async (req, res) => {
|
|
70
|
+
const { page, pageSize, query, livemode } = await searchSchema.validateAsync(req.query, {
|
|
71
|
+
stripUnknown: false,
|
|
72
|
+
allowUnknown: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const where = getWhereFromQuery(query);
|
|
76
|
+
if (typeof livemode === 'boolean') {
|
|
77
|
+
where.livemode = livemode;
|
|
78
|
+
}
|
|
79
|
+
const { rows: list, count } = await Customer.findAndCountAll({
|
|
80
|
+
where,
|
|
81
|
+
order: [['created_at', 'DESC']],
|
|
82
|
+
offset: (page - 1) * pageSize,
|
|
83
|
+
limit: pageSize,
|
|
84
|
+
include: [],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
res.json({ count, list });
|
|
88
|
+
});
|
|
89
|
+
|
|
56
90
|
// eslint-disable-next-line consistent-return
|
|
57
91
|
router.get('/me', user(), async (req, res) => {
|
|
58
92
|
if (!req.user) {
|
|
@@ -4,6 +4,7 @@ import Joi from 'joi';
|
|
|
4
4
|
import type { WhereOptions } from 'sequelize';
|
|
5
5
|
|
|
6
6
|
import { syncStripPayment } from '../integrations/stripe/handlers/payment-intent';
|
|
7
|
+
import { getWhereFromQuery } from '../libs/api';
|
|
7
8
|
import { authenticate } from '../libs/security';
|
|
8
9
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
9
10
|
import { Customer } from '../store/models/customer';
|
|
@@ -25,7 +26,7 @@ const authPortal = authenticate<PaymentIntent>({
|
|
|
25
26
|
},
|
|
26
27
|
});
|
|
27
28
|
|
|
28
|
-
// list payment
|
|
29
|
+
// list payment intents
|
|
29
30
|
const paginationSchema = Joi.object<{
|
|
30
31
|
page: number;
|
|
31
32
|
pageSize: number;
|
|
@@ -102,6 +103,43 @@ router.get('/', authMine, async (req, res) => {
|
|
|
102
103
|
}
|
|
103
104
|
});
|
|
104
105
|
|
|
106
|
+
// search payment intents
|
|
107
|
+
const searchSchema = Joi.object<{
|
|
108
|
+
page: number;
|
|
109
|
+
pageSize: number;
|
|
110
|
+
query: string;
|
|
111
|
+
livemode?: boolean;
|
|
112
|
+
}>({
|
|
113
|
+
page: Joi.number().integer().min(1).default(1),
|
|
114
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
115
|
+
query: Joi.string(),
|
|
116
|
+
livemode: Joi.boolean().empty(''),
|
|
117
|
+
});
|
|
118
|
+
router.get('/search', authMine, async (req, res) => {
|
|
119
|
+
const { page, pageSize, query, livemode } = await searchSchema.validateAsync(req.query, {
|
|
120
|
+
stripUnknown: false,
|
|
121
|
+
allowUnknown: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const where = getWhereFromQuery(query);
|
|
125
|
+
if (typeof livemode === 'boolean') {
|
|
126
|
+
where.livemode = livemode;
|
|
127
|
+
}
|
|
128
|
+
const { rows: list, count } = await Subscription.findAndCountAll({
|
|
129
|
+
where,
|
|
130
|
+
order: [['created_at', 'DESC']],
|
|
131
|
+
offset: (page - 1) * pageSize,
|
|
132
|
+
limit: pageSize,
|
|
133
|
+
include: [
|
|
134
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
135
|
+
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
136
|
+
{ model: Customer, as: 'customer' },
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
res.json({ count, list });
|
|
141
|
+
});
|
|
142
|
+
|
|
105
143
|
router.get('/:id', authPortal, async (req, res) => {
|
|
106
144
|
try {
|
|
107
145
|
const doc = await PaymentIntent.findOne({
|
package/api/src/routes/prices.ts
CHANGED
|
@@ -4,6 +4,7 @@ import Joi from 'joi';
|
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
5
|
import type { WhereOptions } from 'sequelize';
|
|
6
6
|
|
|
7
|
+
import { getWhereFromQuery } from '../libs/api';
|
|
7
8
|
import { authenticate } from '../libs/security';
|
|
8
9
|
import { canUpsell } from '../libs/session';
|
|
9
10
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
@@ -40,6 +41,98 @@ export async function getExpandedPrice(id: string) {
|
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// list prices
|
|
45
|
+
const paginationSchema = Joi.object<{
|
|
46
|
+
page: number;
|
|
47
|
+
pageSize: number;
|
|
48
|
+
livemode?: boolean;
|
|
49
|
+
active?: boolean;
|
|
50
|
+
type?: string;
|
|
51
|
+
currency_id?: string;
|
|
52
|
+
product_id?: string;
|
|
53
|
+
lookup_key?: string;
|
|
54
|
+
}>({
|
|
55
|
+
page: Joi.number().integer().min(1).default(1),
|
|
56
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
57
|
+
livemode: Joi.boolean().empty(''),
|
|
58
|
+
active: Joi.boolean().empty(''),
|
|
59
|
+
type: Joi.string().empty(''),
|
|
60
|
+
currency_id: Joi.string().empty(''),
|
|
61
|
+
product_id: Joi.string().empty(''),
|
|
62
|
+
lookup_key: Joi.string().empty(''),
|
|
63
|
+
});
|
|
64
|
+
router.get('/', auth, async (req, res) => {
|
|
65
|
+
const { page, pageSize, active, livemode, ...query } = await paginationSchema.validateAsync(req.query, {
|
|
66
|
+
stripUnknown: false,
|
|
67
|
+
allowUnknown: true,
|
|
68
|
+
});
|
|
69
|
+
const where: WhereOptions<Price> = {};
|
|
70
|
+
|
|
71
|
+
if (typeof active === 'boolean') {
|
|
72
|
+
where.active = active;
|
|
73
|
+
}
|
|
74
|
+
if (typeof livemode === 'boolean') {
|
|
75
|
+
where.livemode = livemode;
|
|
76
|
+
}
|
|
77
|
+
['type', 'currency_id', 'product_id', 'lookup_key'].forEach((key: string) => {
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
if (query[key]) {
|
|
80
|
+
// @ts-ignore
|
|
81
|
+
where[key] = query[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
Object.keys(query)
|
|
86
|
+
.filter((x) => x.startsWith('recurring.'))
|
|
87
|
+
.forEach((key: string) => {
|
|
88
|
+
// @ts-ignore
|
|
89
|
+
where[key] = query[key];
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const { rows, count } = await Price.findAndCountAll({
|
|
93
|
+
where,
|
|
94
|
+
attributes: ['id'],
|
|
95
|
+
order: [['created_at', 'DESC']],
|
|
96
|
+
offset: (page - 1) * pageSize,
|
|
97
|
+
limit: pageSize,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
res.json({ count, list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))) });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// search prices
|
|
104
|
+
const searchSchema = Joi.object<{
|
|
105
|
+
page: number;
|
|
106
|
+
pageSize: number;
|
|
107
|
+
query: string;
|
|
108
|
+
livemode?: boolean;
|
|
109
|
+
}>({
|
|
110
|
+
page: Joi.number().integer().min(1).default(1),
|
|
111
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
112
|
+
query: Joi.string(),
|
|
113
|
+
livemode: Joi.boolean().empty(''),
|
|
114
|
+
});
|
|
115
|
+
router.get('/search', auth, async (req, res) => {
|
|
116
|
+
const { page, pageSize, query, livemode } = await searchSchema.validateAsync(req.query, {
|
|
117
|
+
stripUnknown: false,
|
|
118
|
+
allowUnknown: true,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const where = getWhereFromQuery(query);
|
|
122
|
+
if (typeof livemode === 'boolean') {
|
|
123
|
+
where.livemode = livemode;
|
|
124
|
+
}
|
|
125
|
+
const { rows, count } = await Price.findAndCountAll({
|
|
126
|
+
where,
|
|
127
|
+
attributes: ['id'],
|
|
128
|
+
order: [['created_at', 'DESC']],
|
|
129
|
+
offset: (page - 1) * pageSize,
|
|
130
|
+
limit: pageSize,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
res.json({ count, list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))) });
|
|
134
|
+
});
|
|
135
|
+
|
|
43
136
|
// FIXME: @wangshijun use schema validation
|
|
44
137
|
// create price
|
|
45
138
|
// eslint-disable-next-line consistent-return
|
|
@@ -218,63 +311,4 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
218
311
|
return res.json(price);
|
|
219
312
|
});
|
|
220
313
|
|
|
221
|
-
// list products and prices
|
|
222
|
-
const paginationSchema = Joi.object<{
|
|
223
|
-
page: number;
|
|
224
|
-
pageSize: number;
|
|
225
|
-
livemode?: boolean;
|
|
226
|
-
active?: boolean;
|
|
227
|
-
type?: string;
|
|
228
|
-
currency_id?: string;
|
|
229
|
-
product_id?: string;
|
|
230
|
-
lookup_key?: string;
|
|
231
|
-
}>({
|
|
232
|
-
page: Joi.number().integer().min(1).default(1),
|
|
233
|
-
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
234
|
-
livemode: Joi.boolean().empty(''),
|
|
235
|
-
active: Joi.boolean().empty(''),
|
|
236
|
-
type: Joi.string().empty(''),
|
|
237
|
-
currency_id: Joi.string().empty(''),
|
|
238
|
-
product_id: Joi.string().empty(''),
|
|
239
|
-
lookup_key: Joi.string().empty(''),
|
|
240
|
-
});
|
|
241
|
-
router.get('/', auth, async (req, res) => {
|
|
242
|
-
const { page, pageSize, active, livemode, ...query } = await paginationSchema.validateAsync(req.query, {
|
|
243
|
-
stripUnknown: false,
|
|
244
|
-
allowUnknown: true,
|
|
245
|
-
});
|
|
246
|
-
const where: WhereOptions<Price> = {};
|
|
247
|
-
|
|
248
|
-
if (typeof active === 'boolean') {
|
|
249
|
-
where.active = active;
|
|
250
|
-
}
|
|
251
|
-
if (typeof livemode === 'boolean') {
|
|
252
|
-
where.livemode = livemode;
|
|
253
|
-
}
|
|
254
|
-
['type', 'currency_id', 'product_id', 'lookup_key'].forEach((key: string) => {
|
|
255
|
-
// @ts-ignore
|
|
256
|
-
if (query[key]) {
|
|
257
|
-
// @ts-ignore
|
|
258
|
-
where[key] = query[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
Object.keys(query)
|
|
263
|
-
.filter((x) => x.startsWith('recurring.'))
|
|
264
|
-
.forEach((key: string) => {
|
|
265
|
-
// @ts-ignore
|
|
266
|
-
where[key] = query[key];
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
const { rows, count } = await Price.findAndCountAll({
|
|
270
|
-
where,
|
|
271
|
-
attributes: ['id'],
|
|
272
|
-
order: [['created_at', 'DESC']],
|
|
273
|
-
offset: (page - 1) * pageSize,
|
|
274
|
-
limit: pageSize,
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
res.json({ count, list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))) });
|
|
278
|
-
});
|
|
279
|
-
|
|
280
314
|
export default router;
|
|
@@ -4,6 +4,7 @@ import Joi from 'joi';
|
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
5
|
import type { WhereOptions } from 'sequelize';
|
|
6
6
|
|
|
7
|
+
import { getWhereFromQuery } from '../libs/api';
|
|
7
8
|
import { authenticate } from '../libs/security';
|
|
8
9
|
import { formatMetadata } from '../libs/util';
|
|
9
10
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
@@ -140,6 +141,39 @@ router.get('/', auth, async (req, res) => {
|
|
|
140
141
|
res.json({ count, list });
|
|
141
142
|
});
|
|
142
143
|
|
|
144
|
+
// search products
|
|
145
|
+
const searchSchema = Joi.object<{
|
|
146
|
+
page: number;
|
|
147
|
+
pageSize: number;
|
|
148
|
+
query: string;
|
|
149
|
+
livemode?: boolean;
|
|
150
|
+
}>({
|
|
151
|
+
page: Joi.number().integer().min(1).default(1),
|
|
152
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
153
|
+
query: Joi.string(),
|
|
154
|
+
livemode: Joi.boolean().empty(''),
|
|
155
|
+
});
|
|
156
|
+
router.get('/search', auth, async (req, res) => {
|
|
157
|
+
const { page, pageSize, query, livemode } = await searchSchema.validateAsync(req.query, {
|
|
158
|
+
stripUnknown: false,
|
|
159
|
+
allowUnknown: true,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const where = getWhereFromQuery(query);
|
|
163
|
+
if (typeof livemode === 'boolean') {
|
|
164
|
+
where.livemode = livemode;
|
|
165
|
+
}
|
|
166
|
+
const { rows: list, count } = await Product.findAndCountAll({
|
|
167
|
+
where,
|
|
168
|
+
order: [['created_at', 'DESC']],
|
|
169
|
+
offset: (page - 1) * pageSize,
|
|
170
|
+
limit: pageSize,
|
|
171
|
+
include: [{ model: Price, as: 'prices' }],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
res.json({ count, list });
|
|
175
|
+
});
|
|
176
|
+
|
|
143
177
|
// get product detail
|
|
144
178
|
router.get('/:id', auth, async (req, res) => {
|
|
145
179
|
res.json(await Product.expand(req.params.id as string));
|
|
@@ -7,6 +7,7 @@ import pick from 'lodash/pick';
|
|
|
7
7
|
import uniq from 'lodash/uniq';
|
|
8
8
|
import { Transaction, WhereOptions } from 'sequelize';
|
|
9
9
|
|
|
10
|
+
import { getWhereFromQuery } from '../libs/api';
|
|
10
11
|
import dayjs from '../libs/dayjs';
|
|
11
12
|
import logger from '../libs/logger';
|
|
12
13
|
import { authenticate } from '../libs/security';
|
|
@@ -134,6 +135,44 @@ router.get('/', authMine, async (req, res) => {
|
|
|
134
135
|
}
|
|
135
136
|
});
|
|
136
137
|
|
|
138
|
+
// search subscriptions
|
|
139
|
+
const searchSchema = Joi.object<{
|
|
140
|
+
page: number;
|
|
141
|
+
pageSize: number;
|
|
142
|
+
query: string;
|
|
143
|
+
livemode?: boolean;
|
|
144
|
+
}>({
|
|
145
|
+
page: Joi.number().integer().min(1).default(1),
|
|
146
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
147
|
+
query: Joi.string(),
|
|
148
|
+
livemode: Joi.boolean().empty(''),
|
|
149
|
+
});
|
|
150
|
+
router.get('/search', auth, async (req, res) => {
|
|
151
|
+
const { page, pageSize, query, livemode } = await searchSchema.validateAsync(req.query, {
|
|
152
|
+
stripUnknown: false,
|
|
153
|
+
allowUnknown: true,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const where = getWhereFromQuery(query);
|
|
157
|
+
if (typeof livemode === 'boolean') {
|
|
158
|
+
where.livemode = livemode;
|
|
159
|
+
}
|
|
160
|
+
const { rows: list, count } = await Subscription.findAndCountAll({
|
|
161
|
+
where,
|
|
162
|
+
order: [['created_at', 'DESC']],
|
|
163
|
+
offset: (page - 1) * pageSize,
|
|
164
|
+
limit: pageSize,
|
|
165
|
+
include: [
|
|
166
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
167
|
+
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
168
|
+
{ model: SubscriptionItem, as: 'items' },
|
|
169
|
+
{ model: Customer, as: 'customer' },
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
res.json({ count, list });
|
|
174
|
+
});
|
|
175
|
+
|
|
137
176
|
// FIXME: exclude some sensitive fields from PaymentMethod
|
|
138
177
|
router.get('/:id', authPortal, async (req, res) => {
|
|
139
178
|
try {
|
|
@@ -392,22 +431,26 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
392
431
|
|
|
393
432
|
// handle updates
|
|
394
433
|
const updates: Partial<TSubscription> = {};
|
|
434
|
+
|
|
435
|
+
// only metadata can be updated when immutable
|
|
395
436
|
if (req.body.metadata) {
|
|
396
437
|
updates.metadata = formatMetadata(req.body.metadata);
|
|
397
438
|
}
|
|
398
439
|
if (subscription.isImmutable()) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
return res.json({ subscription });
|
|
402
|
-
}
|
|
403
|
-
return res.status(400).json({ error: 'Only metadata can be updated when subscription is immutable' });
|
|
404
|
-
}
|
|
405
|
-
if (subscription.isActive() === false) {
|
|
406
|
-
return res.status(400).json({ error: 'Subscription can only be updated when active' });
|
|
440
|
+
await subscription.update(updates);
|
|
441
|
+
return res.json(subscription);
|
|
407
442
|
}
|
|
443
|
+
|
|
444
|
+
// only metadata + description can be updated when not active
|
|
408
445
|
if (req.body.description) {
|
|
409
446
|
updates.description = req.body.description;
|
|
410
447
|
}
|
|
448
|
+
if (subscription.isActive() === false) {
|
|
449
|
+
await subscription.update(updates);
|
|
450
|
+
return res.json(subscription);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// other updates are allowed
|
|
411
454
|
if (req.body.payment_behavior) {
|
|
412
455
|
updates.payment_behavior = req.body.payment_behavior;
|
|
413
456
|
}
|
|
@@ -671,8 +714,7 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
671
714
|
}
|
|
672
715
|
|
|
673
716
|
await subscription.update(updates);
|
|
674
|
-
|
|
675
|
-
return res.json({ subscription, invoice });
|
|
717
|
+
return res.json(subscription);
|
|
676
718
|
});
|
|
677
719
|
} catch (err) {
|
|
678
720
|
console.error(err);
|
|
@@ -5,8 +5,8 @@ export type Pagination<T = any> = T & {
|
|
|
5
5
|
// offset based
|
|
6
6
|
page?: number;
|
|
7
7
|
pageSize?: number;
|
|
8
|
-
// TODO: cursor based
|
|
9
8
|
limit?: number;
|
|
9
|
+
// TODO: cursor based
|
|
10
10
|
starting_after?: string;
|
|
11
11
|
ending_before?: string;
|
|
12
12
|
|
|
@@ -14,6 +14,10 @@ export type Pagination<T = any> = T & {
|
|
|
14
14
|
[key: string]: any;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
export type Searchable<T = any> = Pagination<T> & {
|
|
18
|
+
query: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
17
21
|
export type Paginated<T = any> = {
|
|
18
22
|
count: number;
|
|
19
23
|
list: T[];
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Op } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
import { getWhereFromQuery } from '../../src/libs/api';
|
|
4
|
+
|
|
5
|
+
describe('getWhereFromQuery', () => {
|
|
6
|
+
it('should correctly parse > operator', () => {
|
|
7
|
+
const result = getWhereFromQuery('response_status > 200');
|
|
8
|
+
expect(result).toEqual({ response_status: { [Op.gt]: 200 } });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should correctly parse < operator', () => {
|
|
12
|
+
const result = getWhereFromQuery('retry_count < 5');
|
|
13
|
+
expect(result).toEqual({ retry_count: { [Op.lt]: 5 } });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should correctly parse >= operator', () => {
|
|
17
|
+
const result = getWhereFromQuery('response_status >= 200');
|
|
18
|
+
expect(result).toEqual({ response_status: { [Op.gte]: 200 } });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should correctly parse <= operator', () => {
|
|
22
|
+
const result = getWhereFromQuery('retry_count <= 5');
|
|
23
|
+
expect(result).toEqual({ retry_count: { [Op.lte]: 5 } });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should correctly parse = operator', () => {
|
|
27
|
+
const result = getWhereFromQuery("status = 'succeeded'");
|
|
28
|
+
expect(result).toEqual({ status: 'succeeded' });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should correctly parse != operator', () => {
|
|
32
|
+
const result = getWhereFromQuery("status != 'failed'");
|
|
33
|
+
expect(result).toEqual({ status: { [Op.ne]: 'failed' } });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should correctly parse IS operator', () => {
|
|
37
|
+
const result = getWhereFromQuery("status IS 'failed'");
|
|
38
|
+
expect(result).toEqual({ status: 'failed' });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should correctly parse IS NOT operator', () => {
|
|
42
|
+
const result = getWhereFromQuery("status IS NOT 'failed'");
|
|
43
|
+
expect(result).toEqual({ status: { [Op.ne]: 'failed' } });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should correctly parse IS NOT operator: NULL', () => {
|
|
47
|
+
const result = getWhereFromQuery('status IS NOT null');
|
|
48
|
+
expect(result).toEqual({ status: { [Op.ne]: null } });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should correctly parse LIKE operator', () => {
|
|
52
|
+
const result = getWhereFromQuery("status LIKE 'fail'");
|
|
53
|
+
expect(result).toEqual({ status: { [Op.like]: 'fail' } });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should correctly parse NOT LIKE operator', () => {
|
|
57
|
+
const result = getWhereFromQuery("status NOT LIKE 'fail'");
|
|
58
|
+
expect(result).toEqual({ status: { [Op.notLike]: 'fail' } });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should correctly parse IN operator', () => {
|
|
62
|
+
const result = getWhereFromQuery('status in (1,2,3)');
|
|
63
|
+
expect(result).toEqual({ status: { [Op.in]: [1, 2, 3] } });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should correctly parse NOT IN operator', () => {
|
|
67
|
+
const result = getWhereFromQuery('status NOT IN (1,2,3)');
|
|
68
|
+
expect(result).toEqual({ status: { [Op.notIn]: [1, 2, 3] } });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should correctly parse BETWEEN operator', () => {
|
|
72
|
+
const result = getWhereFromQuery('age BETWEEN 1 AND 3');
|
|
73
|
+
expect(result).toEqual({ age: { [Op.between]: [1, 3] } });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should correctly parse NOT BETWEEN operator', () => {
|
|
77
|
+
const result = getWhereFromQuery('age NOT BETWEEN 1 AND 3');
|
|
78
|
+
expect(result).toEqual({ age: { [Op.notBetween]: [1, 3] } });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should correctly parse AND operator', () => {
|
|
82
|
+
const result = getWhereFromQuery('response_status >= 200 AND response_status < 300');
|
|
83
|
+
expect(result).toEqual({
|
|
84
|
+
[Op.and]: [{ response_status: { [Op.gte]: 200 } }, { response_status: { [Op.lt]: 300 } }],
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should correctly parse AND operator with different field types', () => {
|
|
89
|
+
const result = getWhereFromQuery("status = 'succeeded' AND response_status = 200");
|
|
90
|
+
expect(result).toEqual({ [Op.and]: [{ status: 'succeeded' }, { response_status: 200 }] });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should correctly parse AND operator for BETWEEN condition', () => {
|
|
94
|
+
const result = getWhereFromQuery('retry_count >= 1 AND retry_count <= 3');
|
|
95
|
+
expect(result).toEqual({ [Op.and]: [{ retry_count: { [Op.gte]: 1 } }, { retry_count: { [Op.lte]: 3 } }] });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should correctly parse OR operator with different field types', () => {
|
|
99
|
+
const result = getWhereFromQuery("status = 'succeeded' OR response_status = 200");
|
|
100
|
+
expect(result).toEqual({ [Op.or]: [{ status: 'succeeded' }, { response_status: 200 }] });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should correctly parse OR operator with Boolean and Number fields', () => {
|
|
104
|
+
const result = getWhereFromQuery('livemode = true OR retry_count > 2');
|
|
105
|
+
expect(result).toEqual({ [Op.or]: [{ livemode: true }, { retry_count: { [Op.gt]: 2 } }] });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should correctly parse OR operator with boolean,number,string fields', () => {
|
|
109
|
+
const result = getWhereFromQuery('livemode = true OR retry_count > 2 OR status = "succeeded"');
|
|
110
|
+
expect(result).toEqual({
|
|
111
|
+
[Op.or]: [{ [Op.or]: [{ livemode: true }, { retry_count: { [Op.gt]: 2 } }] }, { status: 'succeeded' }],
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should correctly parse OR operator with two String fields', () => {
|
|
116
|
+
const result = getWhereFromQuery("event_id LIKE '%event%' OR status != 'failed'");
|
|
117
|
+
expect(result).toEqual({ [Op.or]: [{ event_id: { [Op.like]: '%event%' } }, { status: { [Op.ne]: 'failed' } }] });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should throw an error when the query is empty', () => {
|
|
121
|
+
expect(() => getWhereFromQuery('')).toThrow(/empty query/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should correctly parse AND operator x3 for metadata', () => {
|
|
125
|
+
const result = getWhereFromQuery(
|
|
126
|
+
// 'metadata.key IS NULL AND metadata.nftId="3" AND status IN ("succeeded","failed")'
|
|
127
|
+
'metadata.key IS NULL AND metadata.nftId="3" AND metadata.appId="4"'
|
|
128
|
+
);
|
|
129
|
+
expect(result).toEqual({
|
|
130
|
+
[Op.and]: [
|
|
131
|
+
{
|
|
132
|
+
[Op.and]: [{ 'metadata.key': null }, { 'metadata.nftId': '3' }],
|
|
133
|
+
},
|
|
134
|
+
{ 'metadata.appId': '4' },
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should correctly parse AND + IN operator for metadata', () => {
|
|
140
|
+
const result = getWhereFromQuery(
|
|
141
|
+
'metadata.key IS NULL AND metadata.nftId="3" AND status IN ("succeeded","failed")'
|
|
142
|
+
);
|
|
143
|
+
expect(result).toEqual({
|
|
144
|
+
[Op.and]: [
|
|
145
|
+
{
|
|
146
|
+
[Op.and]: [{ 'metadata.key': null }, { 'metadata.nftId': '3' }],
|
|
147
|
+
},
|
|
148
|
+
{ status: { [Op.in]: ['succeeded', 'failed'] } },
|
|
149
|
+
],
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// eslint-disable-next-line jest/no-disabled-tests
|
|
154
|
+
it.skip('should throw an error when an unsupported operator is used', () => {
|
|
155
|
+
expect(() => getWhereFromQuery('status === "failed"')).toThrow(/Operator not supported/);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCheckoutMode,
|
|
3
|
+
getPriceCurrencyOptions,
|
|
4
|
+
getPriceUintAmountByCurrency,
|
|
5
|
+
getRecurringPeriod,
|
|
6
|
+
} from '../../src/libs/session';
|
|
7
|
+
import type { TLineItemExpanded } from '../../src/store/models';
|
|
8
|
+
|
|
9
|
+
describe('getCheckoutMode', () => {
|
|
10
|
+
it('should return "setup" when items array is empty', () => {
|
|
11
|
+
const result = getCheckoutMode([]);
|
|
12
|
+
expect(result).toBe('setup');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should return "setup" when all items are metered', () => {
|
|
16
|
+
const items = [
|
|
17
|
+
{ price: { type: 'recurring', recurring: { usage_type: 'metered' } } },
|
|
18
|
+
{ price: { type: 'recurring', recurring: { usage_type: 'metered' } } },
|
|
19
|
+
];
|
|
20
|
+
const result = getCheckoutMode(items as TLineItemExpanded[]);
|
|
21
|
+
expect(result).toBe('setup');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should return "subscription" when some items are recurring', () => {
|
|
25
|
+
const items = [
|
|
26
|
+
{ price: { type: 'recurring', recurring: { usage_type: 'metered' } } },
|
|
27
|
+
{ price: { type: 'recurring' } },
|
|
28
|
+
];
|
|
29
|
+
const result = getCheckoutMode(items as TLineItemExpanded[]);
|
|
30
|
+
expect(result).toBe('subscription');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should return "payment" when none of the above conditions are met', () => {
|
|
34
|
+
const items = [{ price: { type: 'one_time' } }, { price: { type: 'one_time' } }];
|
|
35
|
+
const result = getCheckoutMode(items as TLineItemExpanded[]);
|
|
36
|
+
expect(result).toBe('payment');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('getPriceCurrencyOptions', () => {
|
|
41
|
+
it('should return currency_options when it is an array', () => {
|
|
42
|
+
const price = {
|
|
43
|
+
currency_options: [
|
|
44
|
+
{ currency_id: 'usd', unit_amount: 1000, tiers: null, custom_unit_amount: null },
|
|
45
|
+
{ currency_id: 'eur', unit_amount: 900, tiers: null, custom_unit_amount: null },
|
|
46
|
+
],
|
|
47
|
+
currency_id: 'usd',
|
|
48
|
+
unit_amount: 1000,
|
|
49
|
+
};
|
|
50
|
+
const result = getPriceCurrencyOptions(price as any);
|
|
51
|
+
expect(result).toEqual(price.currency_options);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return an array with a single object when currency_options is not an array', () => {
|
|
55
|
+
const price = {
|
|
56
|
+
currency_id: 'usd',
|
|
57
|
+
unit_amount: 1000,
|
|
58
|
+
};
|
|
59
|
+
const result = getPriceCurrencyOptions(price as any);
|
|
60
|
+
expect(result).toEqual([{ currency_id: 'usd', unit_amount: 1000, tiers: null, custom_unit_amount: null }]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('getPriceUintAmountByCurrency', () => {
|
|
64
|
+
it('should return the unit_amount of the matching currency_id in currency_options', () => {
|
|
65
|
+
const price = {
|
|
66
|
+
currency_options: [
|
|
67
|
+
{ currency_id: 'usd', unit_amount: 1000 },
|
|
68
|
+
{ currency_id: 'eur', unit_amount: 900 },
|
|
69
|
+
],
|
|
70
|
+
currency_id: 'usd',
|
|
71
|
+
unit_amount: 1000,
|
|
72
|
+
};
|
|
73
|
+
const result = getPriceUintAmountByCurrency(price as any, 'eur');
|
|
74
|
+
expect(result).toBe(900);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return the unit_amount of the price object when the currency_id is not found in currency_options', () => {
|
|
78
|
+
const price = {
|
|
79
|
+
currency_options: [
|
|
80
|
+
{ currency_id: 'usd', unit_amount: 1000 },
|
|
81
|
+
{ currency_id: 'eur', unit_amount: 900 },
|
|
82
|
+
],
|
|
83
|
+
currency_id: 'usd',
|
|
84
|
+
unit_amount: 1000,
|
|
85
|
+
};
|
|
86
|
+
const result = getPriceUintAmountByCurrency(price as any, 'gbp');
|
|
87
|
+
expect(result).toBe(1000);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should return the unit_amount of the price object when currency_options is not an array', () => {
|
|
91
|
+
const price = {
|
|
92
|
+
currency_id: 'usd',
|
|
93
|
+
unit_amount: 1000,
|
|
94
|
+
};
|
|
95
|
+
const result = getPriceUintAmountByCurrency(price as any, 'eur');
|
|
96
|
+
expect(result).toBe(1000);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('getRecurringPeriod', () => {
|
|
100
|
+
it('should return the correct period in milliseconds when interval is "hour"', () => {
|
|
101
|
+
const recurring = { interval: 'hour', interval_count: '1' };
|
|
102
|
+
const result = getRecurringPeriod(recurring as any);
|
|
103
|
+
expect(result).toBe(60 * 60 * 1000);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should return the correct period in milliseconds when interval is "day"', () => {
|
|
107
|
+
const recurring = { interval: 'day', interval_count: '1' };
|
|
108
|
+
const result = getRecurringPeriod(recurring as any);
|
|
109
|
+
expect(result).toBe(24 * 60 * 60 * 1000);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return the correct period in milliseconds when interval is "week"', () => {
|
|
113
|
+
const recurring = { interval: 'week', interval_count: '1' };
|
|
114
|
+
const result = getRecurringPeriod(recurring as any);
|
|
115
|
+
expect(result).toBe(7 * 24 * 60 * 60 * 1000);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should return the correct period in milliseconds when interval is "month"', () => {
|
|
119
|
+
const recurring = { interval: 'month', interval_count: '1' };
|
|
120
|
+
const result = getRecurringPeriod(recurring as any);
|
|
121
|
+
expect(result).toBe(30 * 24 * 60 * 60 * 1000);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should return the correct period in milliseconds when interval is "year"', () => {
|
|
125
|
+
const recurring = { interval: 'year', interval_count: '1' };
|
|
126
|
+
const result = getRecurringPeriod(recurring as any);
|
|
127
|
+
expect(result).toBe(365 * 24 * 60 * 60 * 1000);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should return 0 when interval is an invalid value', () => {
|
|
131
|
+
const recurring = { interval: 'invalid', interval_count: '1' };
|
|
132
|
+
const result = getRecurringPeriod(recurring as any);
|
|
133
|
+
expect(result).toBe(0);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import dayjs from '../../src/libs/dayjs';
|
|
2
|
+
import {
|
|
3
|
+
createCodeGenerator,
|
|
4
|
+
createIdGenerator,
|
|
5
|
+
formatMetadata,
|
|
6
|
+
getMetadataFromQuery,
|
|
7
|
+
getNextRetry,
|
|
8
|
+
tryWithTimeout,
|
|
9
|
+
} from '../../src/libs/util';
|
|
10
|
+
|
|
11
|
+
describe('createIdGenerator', () => {
|
|
12
|
+
it('should return a function that generates an ID with the specified prefix and size', () => {
|
|
13
|
+
const generateId = createIdGenerator('test', 10);
|
|
14
|
+
const id = generateId();
|
|
15
|
+
expect(id.startsWith('test_')).toBe(true);
|
|
16
|
+
expect(id.length).toBe(15); // 10 for ID, 4 for prefix, 1 for underscore
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should return a function that generates an ID with the specified prefix and default size when size is not provided', () => {
|
|
20
|
+
const generateId = createIdGenerator('test');
|
|
21
|
+
const id = generateId();
|
|
22
|
+
expect(id.startsWith('test_')).toBe(true);
|
|
23
|
+
expect(id.length).toBe(29); // 24 for ID, 4 for prefix, 1 for underscore
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return a function that generates an ID without a prefix when prefix is not provided', () => {
|
|
27
|
+
const generateId = createIdGenerator('', 10);
|
|
28
|
+
const id = generateId();
|
|
29
|
+
expect(id.startsWith('_')).toBe(false);
|
|
30
|
+
expect(id.length).toBe(10);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('createCodeGenerator', () => {
|
|
35
|
+
it('should return a function that generates a code with the specified prefix and size', () => {
|
|
36
|
+
const generateCode = createCodeGenerator('test', 10);
|
|
37
|
+
const code = generateCode();
|
|
38
|
+
expect(code.startsWith('test_')).toBe(true);
|
|
39
|
+
expect(code.length).toBe(15); // 10 for code, 4 for prefix, 1 for underscore
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return a function that generates a code with the specified prefix and default size when size is not provided', () => {
|
|
43
|
+
const generateCode = createCodeGenerator('test');
|
|
44
|
+
const code = generateCode();
|
|
45
|
+
expect(code.startsWith('test_')).toBe(true);
|
|
46
|
+
expect(code.length).toBe(29); // 24 for code, 4 for prefix, 1 for underscore
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return a function that generates a code without a prefix when prefix is not provided', () => {
|
|
50
|
+
const generateCode = createCodeGenerator('', 10);
|
|
51
|
+
const code = generateCode();
|
|
52
|
+
expect(code.startsWith('_')).toBe(false);
|
|
53
|
+
expect(code.length).toBe(10);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('formatMetadata', () => {
|
|
58
|
+
it('should return an empty object when metadata is undefined', () => {
|
|
59
|
+
const result = formatMetadata(undefined);
|
|
60
|
+
expect(result).toEqual({});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return a formatted object when metadata is an array', () => {
|
|
64
|
+
const metadata = [
|
|
65
|
+
{ key: 'key1', value: 'value1' },
|
|
66
|
+
{ key: 'key2', value: '' },
|
|
67
|
+
{ key: 'key3', value: 'value3' },
|
|
68
|
+
];
|
|
69
|
+
const result = formatMetadata(metadata);
|
|
70
|
+
expect(result).toEqual({ key1: 'value1', key3: 'value3' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should return a formatted object when metadata is an object', () => {
|
|
74
|
+
const metadata = {
|
|
75
|
+
key1: 'value1',
|
|
76
|
+
key2: '',
|
|
77
|
+
key3: 'value3',
|
|
78
|
+
};
|
|
79
|
+
const result = formatMetadata(metadata);
|
|
80
|
+
expect(result).toEqual({ key1: 'value1', key3: 'value3' });
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('tryWithTimeout', () => {
|
|
85
|
+
it('should resolve with the result of the async function when it completes before the timeout', async () => {
|
|
86
|
+
const asyncFn = jest.fn().mockResolvedValue('result');
|
|
87
|
+
const result = await tryWithTimeout(asyncFn, 100);
|
|
88
|
+
expect(result).toBe('result');
|
|
89
|
+
expect(asyncFn).toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should reject with a timeout error when the async function does not complete before the timeout', async () => {
|
|
93
|
+
// eslint-disable-next-line no-promise-executor-return
|
|
94
|
+
const asyncFn = jest.fn().mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 200)));
|
|
95
|
+
await expect(tryWithTimeout(asyncFn, 100)).rejects.toThrow('Operation timed out after 100 ms');
|
|
96
|
+
expect(asyncFn).toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should reject with the error of the async function when it rejects', async () => {
|
|
100
|
+
const asyncFn = jest.fn().mockRejectedValue(new Error('error'));
|
|
101
|
+
await expect(tryWithTimeout(asyncFn, 100)).rejects.toThrow('error');
|
|
102
|
+
expect(asyncFn).toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should throw an error when the provided asyncFn is not a function', () => {
|
|
106
|
+
// @ts-ignore
|
|
107
|
+
expect(() => tryWithTimeout(null)).toThrow('Must provide a valid asyncFn function');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('getNextRetry', () => {
|
|
112
|
+
it('should return the current time plus 1 second when retry count is 0', () => {
|
|
113
|
+
const now = dayjs().unix();
|
|
114
|
+
const result = getNextRetry(0);
|
|
115
|
+
expect(result).toBe(now + 1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should return the current time plus 2 seconds when retry count is 1', () => {
|
|
119
|
+
const now = dayjs().unix();
|
|
120
|
+
const result = getNextRetry(1);
|
|
121
|
+
expect(result).toBe(now + 2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should return the current time plus 4 seconds when retry count is 2', () => {
|
|
125
|
+
const now = dayjs().unix();
|
|
126
|
+
const result = getNextRetry(2);
|
|
127
|
+
expect(result).toBe(now + 4);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should return the current time plus 8 seconds when retry count is 3', () => {
|
|
131
|
+
const now = dayjs().unix();
|
|
132
|
+
const result = getNextRetry(3);
|
|
133
|
+
expect(result).toBe(now + 8);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('getMetadataFromQuery', () => {
|
|
138
|
+
it('should return an empty object when the query object is empty', () => {
|
|
139
|
+
const result = getMetadataFromQuery({});
|
|
140
|
+
expect(result).toEqual({});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should return an object with metadata when the query object contains metadata keys', () => {
|
|
144
|
+
const query = {
|
|
145
|
+
'metadata.key1': 'value1',
|
|
146
|
+
'metadata.key2': 'value2',
|
|
147
|
+
};
|
|
148
|
+
const result = getMetadataFromQuery(query);
|
|
149
|
+
expect(result).toEqual({
|
|
150
|
+
key1: 'value1',
|
|
151
|
+
key2: 'value2',
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return an object without non-metadata keys when the query object contains non-metadata keys', () => {
|
|
156
|
+
const query = {
|
|
157
|
+
'metadata.key1': 'value1',
|
|
158
|
+
key2: 'value2',
|
|
159
|
+
};
|
|
160
|
+
const result = getMetadataFromQuery(query);
|
|
161
|
+
expect(result).toEqual({
|
|
162
|
+
key1: 'value1',
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should ignore metadata keys with undefined or null values', () => {
|
|
167
|
+
const query = {
|
|
168
|
+
'metadata.key1': 'value1',
|
|
169
|
+
'metadata.key2': undefined,
|
|
170
|
+
'metadata.key3': null,
|
|
171
|
+
};
|
|
172
|
+
const result = getMetadataFromQuery(query);
|
|
173
|
+
expect(result).toEqual({
|
|
174
|
+
key1: 'value1',
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
package/api/third.d.ts
CHANGED
package/blocklet.yml
CHANGED
package/jest.config.js
CHANGED
|
@@ -3,9 +3,11 @@ module.exports = {
|
|
|
3
3
|
verbose: true,
|
|
4
4
|
preset: 'ts-jest',
|
|
5
5
|
testEnvironment: 'node',
|
|
6
|
-
collectCoverage: true,
|
|
7
6
|
coverageDirectory: 'coverage',
|
|
7
|
+
restoreMocks: true,
|
|
8
8
|
clearMocks: true,
|
|
9
|
+
globalSetup: '@blocklet/sdk/lib/util/jest-setup.js',
|
|
10
|
+
globalTeardown: '@blocklet/sdk/lib/util/jest-teardown.js',
|
|
9
11
|
transform: {
|
|
10
12
|
'^.+\\.ts?$': 'ts-jest',
|
|
11
13
|
},
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.83",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev",
|
|
6
6
|
"eject": "vite eject",
|
|
7
7
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
8
8
|
"lint:fix": "npm run lint -- --fix",
|
|
9
9
|
"format": "prettier -w src",
|
|
10
|
-
"test": "jest",
|
|
10
|
+
"test": "node scripts/jest.js",
|
|
11
11
|
"coverage": "npm run test -- --coverage",
|
|
12
12
|
"start": "cross-env NODE_ENV=development nodemon api/dev.ts -w api",
|
|
13
13
|
"clean": "node scripts/build-clean.js",
|
|
@@ -42,15 +42,15 @@
|
|
|
42
42
|
]
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@abtnode/cron": "
|
|
45
|
+
"@abtnode/cron": "1.16.21-beta-e828f413",
|
|
46
46
|
"@arcblock/did": "^1.18.106",
|
|
47
47
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
48
48
|
"@arcblock/did-connect": "^2.8.23",
|
|
49
49
|
"@arcblock/did-util": "^1.18.106",
|
|
50
50
|
"@arcblock/jwt": "^1.18.106",
|
|
51
51
|
"@arcblock/ux": "^2.8.23",
|
|
52
|
-
"@blocklet/logger": "
|
|
53
|
-
"@blocklet/sdk": "
|
|
52
|
+
"@blocklet/logger": "1.16.21-beta-e828f413",
|
|
53
|
+
"@blocklet/sdk": "1.16.21-beta-e828f413",
|
|
54
54
|
"@blocklet/ui-react": "^2.8.23",
|
|
55
55
|
"@blocklet/uploader": "^0.0.55",
|
|
56
56
|
"@mui/icons-material": "^5.14.19",
|
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
"react-router-dom": "^6.20.1",
|
|
98
98
|
"rimraf": "^3.0.2",
|
|
99
99
|
"sequelize": "^6.35.1",
|
|
100
|
+
"sql-where-parser": "^2.2.1",
|
|
100
101
|
"sqlite3": "^5.1.6",
|
|
101
102
|
"stripe": "^13.11.0",
|
|
102
103
|
"typewriter-effect": "^2.21.0",
|
|
@@ -106,10 +107,10 @@
|
|
|
106
107
|
"validator": "^13.11.0"
|
|
107
108
|
},
|
|
108
109
|
"devDependencies": {
|
|
109
|
-
"@abtnode/types": "
|
|
110
|
+
"@abtnode/types": "1.16.21-beta-e828f413",
|
|
110
111
|
"@arcblock/eslint-config": "^0.2.4",
|
|
111
112
|
"@arcblock/eslint-config-ts": "^0.2.4",
|
|
112
|
-
"@did-pay/types": "1.13.
|
|
113
|
+
"@did-pay/types": "1.13.83",
|
|
113
114
|
"@types/cookie-parser": "^1.4.6",
|
|
114
115
|
"@types/cors": "^2.8.17",
|
|
115
116
|
"@types/dotenv-flow": "^3.3.3",
|
|
@@ -148,5 +149,5 @@
|
|
|
148
149
|
"parser": "typescript"
|
|
149
150
|
}
|
|
150
151
|
},
|
|
151
|
-
"gitHead": "
|
|
152
|
+
"gitHead": "e6357508c38296224f7c493b6630a45fd359d6bc"
|
|
152
153
|
}
|
package/scripts/jest.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require('../../../tools/jest');
|