payment-kit 1.18.21 → 1.18.23

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.
@@ -1,5 +1,6 @@
1
1
  import notification from '@blocklet/sdk/service/notification';
2
2
 
3
+ import dayjs from 'dayjs';
3
4
  import { Customer } from '../../store/models';
4
5
 
5
6
  import logger from '../../libs/logger';
@@ -15,13 +16,17 @@ const handleUserUpdate = async ({ user }: { user: any }) => {
15
16
  },
16
17
  });
17
18
  if (customer) {
19
+ if (Math.abs(dayjs(customer.updated_at).diff(dayjs(user.updatedAt), 'minute')) <= 1) {
20
+ logger.info(`skip customer update due to recent sync within 1 minute: ${customer.did}`);
21
+ return;
22
+ }
18
23
  const now = Math.floor(Date.now() / 1000);
19
24
  await customer.update({
20
- name: user.fullName,
21
- email: user.email,
22
- phone: user.phone,
25
+ name: user.fullName || customer.name,
26
+ email: user.email || customer.email,
27
+ phone: user.phone || customer.phone,
23
28
  last_sync_at: now,
24
- address: Customer.formatAddressFromUser(user),
29
+ address: Customer.formatAddressFromUser(user, customer),
25
30
  });
26
31
  logger.info(`customer info updated: ${customer.did}`);
27
32
  }
@@ -2,6 +2,7 @@ import { BN, fromTokenToUnit } from '@ocap/util';
2
2
  import Joi from 'joi';
3
3
  import { Op } from 'sequelize';
4
4
  import SqlWhereParser from 'sql-where-parser';
5
+ import type { OrderDirection, OrderInput, OrderItem } from '../store/models';
5
6
 
6
7
  const parser = new SqlWhereParser();
7
8
 
@@ -109,36 +110,191 @@ export const getWhereFromKvQuery = (query?: string) => {
109
110
  return out;
110
111
  };
111
112
 
113
+ export function normalizeOrder(
114
+ order: OrderInput | undefined,
115
+ defaultOrder: OrderItem[] = [['created_at', 'DESC']]
116
+ ): OrderItem[] {
117
+ if (!order) return defaultOrder;
118
+
119
+ let normalizedOrder: OrderItem[];
120
+
121
+ if (typeof order === 'string') {
122
+ const [field, direction] = order.split(':');
123
+ normalizedOrder = [[field || '', (direction?.toUpperCase() as OrderDirection) || 'ASC']];
124
+ } else if (Array.isArray(order)) {
125
+ if (order.length === 0) return defaultOrder;
126
+
127
+ if (Array.isArray(order[0])) {
128
+ normalizedOrder = order.map(([field, direction]) => [
129
+ field || '',
130
+ (direction?.toUpperCase() as OrderDirection) || 'ASC',
131
+ ]);
132
+ } else {
133
+ normalizedOrder = order.map((item) => {
134
+ const [field, direction] = (item as string).split(':');
135
+ return [field || '', (direction?.toUpperCase() as OrderDirection) || 'ASC'];
136
+ });
137
+ }
138
+ } else {
139
+ return defaultOrder;
140
+ }
141
+
142
+ // merge default order with normalized order
143
+ const orderFields = new Set(normalizedOrder.map(([field]) => field));
144
+ const remainingDefaultOrders = defaultOrder.filter(([field]) => !orderFields.has(field));
145
+
146
+ return [...normalizedOrder, ...remainingDefaultOrders];
147
+ }
148
+
149
+ export function getOrder(query: Record<string, any>, defaultOrder?: OrderItem[]): OrderItem[] {
150
+ if (query.order) {
151
+ return normalizeOrder(query.order, defaultOrder);
152
+ }
153
+
154
+ if (query.o) {
155
+ const direction = query.o.toUpperCase() as OrderDirection;
156
+ return defaultOrder?.map(([field]) => [field, direction]) || [['created_at', direction]];
157
+ }
158
+
159
+ return defaultOrder || [['created_at', 'DESC']];
160
+ }
161
+
162
+ const fieldRegex = /^[\w.]+$/i;
163
+ const directionRegex = /(asc|desc)$/i;
164
+
165
+ const ORDER_ERROR_MESSAGES = {
166
+ FIELD_PATTERN: 'Field name can only contain letters, numbers, dots and underscores',
167
+ DIRECTION_PATTERN: 'Direction must be either "asc" or "desc"',
168
+ SEPARATOR: 'Must use ":" to separate field and direction',
169
+ TUPLE_LENGTH: 'Each tuple must contain exactly 2 elements',
170
+ ARRAY_TYPE: 'Each item must be an array',
171
+ INVALID_FORMAT:
172
+ 'Invalid order format. Must be one of:\n' +
173
+ '- Single field: "field:direction"\n' +
174
+ '- Multiple fields: ["field1:asc", "field2:desc"]\n' +
175
+ '- Tuple format: [["field1", "ASC"], ["field2", "DESC"]]\n' +
176
+ 'where field contains only letters, numbers, dots and underscores, ' +
177
+ 'and direction is either "asc" or "desc" (case insensitive)',
178
+ };
179
+
180
+ const validateField = (field: any): boolean => typeof field === 'string' && fieldRegex.test(field);
181
+
182
+ const validateDirection = (direction: any): boolean => typeof direction === 'string' && directionRegex.test(direction);
183
+
184
+ // Function to validate the order parameter
185
+ export const validateOrderInput = (value: any, helpers: any) => {
186
+ // If it does not exist or is null, skip validation
187
+ if (value == null) return value;
188
+
189
+ // String format: "field:direction"
190
+ if (typeof value === 'string') {
191
+ const parts = value.split(':');
192
+
193
+ if (parts.length !== 2) {
194
+ return helpers.message(ORDER_ERROR_MESSAGES.SEPARATOR);
195
+ }
196
+
197
+ const [field, direction] = parts;
198
+
199
+ if (!validateField(field)) {
200
+ return helpers.message(ORDER_ERROR_MESSAGES.FIELD_PATTERN);
201
+ }
202
+
203
+ if (!validateDirection(direction)) {
204
+ return helpers.message(ORDER_ERROR_MESSAGES.DIRECTION_PATTERN);
205
+ }
206
+
207
+ return value;
208
+ }
209
+
210
+ // Array format
211
+ if (Array.isArray(value)) {
212
+ // Empty array, return directly
213
+ if (value.length === 0) return value;
214
+
215
+ // String array format
216
+ if (typeof value[0] === 'string') {
217
+ const invalidItem = value.find((item) => typeof item !== 'string');
218
+ if (invalidItem !== undefined) {
219
+ return helpers.message(ORDER_ERROR_MESSAGES.INVALID_FORMAT);
220
+ }
221
+
222
+ for (const item of value) {
223
+ const parts = item.split(':');
224
+
225
+ if (parts.length !== 2) {
226
+ return helpers.message(ORDER_ERROR_MESSAGES.SEPARATOR);
227
+ }
228
+
229
+ const [field, direction] = parts;
230
+
231
+ if (!validateField(field)) {
232
+ return helpers.message(ORDER_ERROR_MESSAGES.FIELD_PATTERN);
233
+ }
234
+
235
+ if (!validateDirection(direction)) {
236
+ return helpers.message(ORDER_ERROR_MESSAGES.DIRECTION_PATTERN);
237
+ }
238
+ }
239
+
240
+ return value;
241
+ }
242
+
243
+ // Multiple tuples format
244
+ if (Array.isArray(value[0])) {
245
+ for (const tuple of value) {
246
+ if (!Array.isArray(tuple)) {
247
+ return helpers.message(ORDER_ERROR_MESSAGES.ARRAY_TYPE);
248
+ }
249
+
250
+ if (tuple.length !== 2) {
251
+ return helpers.message(ORDER_ERROR_MESSAGES.TUPLE_LENGTH);
252
+ }
253
+
254
+ const [field, direction] = tuple;
255
+
256
+ if (!validateField(field)) {
257
+ return helpers.message(ORDER_ERROR_MESSAGES.FIELD_PATTERN);
258
+ }
259
+
260
+ if (!validateDirection(direction)) {
261
+ return helpers.message(ORDER_ERROR_MESSAGES.DIRECTION_PATTERN);
262
+ }
263
+ }
264
+
265
+ return value;
266
+ }
267
+ }
268
+ return helpers.message(ORDER_ERROR_MESSAGES.INVALID_FORMAT);
269
+ };
270
+
112
271
  export function createListParamSchema<T>(schema: any, pageSize: number = 20) {
113
- return Joi.object<T & { page: number; pageSize: number; livemode?: boolean; q?: string; o?: string }>({
114
- // prettier-ignore
272
+ return Joi.object<
273
+ T & {
274
+ page: number;
275
+ pageSize: number;
276
+ livemode?: boolean;
277
+ q?: string;
278
+ o?: string;
279
+ order?: OrderInput;
280
+ }
281
+ >({
115
282
  page: Joi.number()
116
283
  .integer()
117
284
  .default(1)
118
- .custom((value) => {
119
- if (value < 1) {
120
- return 1;
121
- }
122
- return value;
123
- }, 'page should be valid'),
124
-
285
+ .custom((value) => (value < 1 ? 1 : value), 'page should be valid'),
125
286
  pageSize: Joi.number()
126
287
  .integer()
127
288
  .default(pageSize)
128
289
  .custom((value) => {
129
- if (value > 100) {
130
- return 100;
131
- }
132
- if (value < 1) {
133
- return 1;
134
- }
290
+ if (value > 100) return 100;
291
+ if (value < 1) return 1;
135
292
  return value;
136
293
  }, 'pageSize should be valid'),
137
-
138
294
  livemode: Joi.boolean().empty(''),
139
- q: Joi.string().empty(''), // query
140
- o: Joi.string().empty(''), // order
141
-
295
+ q: Joi.string().empty(''),
296
+ o: Joi.string().valid('asc', 'desc').insensitive().empty(''),
297
+ order: Joi.any().custom(validateOrderInput).optional(),
142
298
  ...schema,
143
299
  });
144
300
  }
@@ -1312,7 +1312,6 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
1312
1312
  lineItems
1313
1313
  );
1314
1314
 
1315
- // 返回支付信息
1316
1315
  return res.json({
1317
1316
  paymentIntent,
1318
1317
  checkoutSession,
@@ -7,7 +7,7 @@ import isEmail from 'validator/es/lib/isEmail';
7
7
  import { Op } from 'sequelize';
8
8
  import { BN } from '@ocap/util';
9
9
  import { getStakeSummaryByDid, getTokenSummaryByDid, getTokenByAddress } from '../integrations/arcblock/stake';
10
- import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
10
+ import { createListParamSchema, getOrder, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
11
11
  import { authenticate } from '../libs/security';
12
12
  import { formatMetadata } from '../libs/util';
13
13
  import { Customer } from '../store/models/customer';
@@ -52,7 +52,7 @@ router.get('/', auth, async (req, res) => {
52
52
  try {
53
53
  const { rows: list, count } = await Customer.findAndCountAll({
54
54
  where,
55
- order: [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']],
55
+ order: getOrder(query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
56
56
  offset: (page - 1) * pageSize,
57
57
  limit: pageSize,
58
58
  include: [],
@@ -83,7 +83,7 @@ router.get('/search', auth, async (req, res) => {
83
83
  }
84
84
  const { rows: list, count } = await Customer.findAndCountAll({
85
85
  where,
86
- order: [['created_at', o === 'asc' ? 'ASC' : 'DESC']],
86
+ order: getOrder(req.query, [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
87
87
  offset: (page - 1) * pageSize,
88
88
  limit: pageSize,
89
89
  include: [],
@@ -104,7 +104,7 @@ router.get('/me', sessionMiddleware(), async (req, res) => {
104
104
  if (!doc) {
105
105
  if (req.query.fallback) {
106
106
  const result = await blocklet.getUser(req.user.did);
107
- return res.json({ ...result.user, address: {}, livemode });
107
+ return res.json({ ...result.user, address: Customer.formatAddressFromUser(result.user), livemode });
108
108
  }
109
109
  if (req.query.create) {
110
110
  // create customer
@@ -132,6 +132,9 @@ router.get('/me', sessionMiddleware(), async (req, res) => {
132
132
  return res.json({ error: 'Customer not found' });
133
133
  }
134
134
  }
135
+ if (req.query.skipSummary) {
136
+ return res.json({ ...doc.toJSON(), livemode });
137
+ }
135
138
  try {
136
139
  const [summary, stake, token] = await Promise.all([
137
140
  doc.getSummary(livemode),
@@ -2,7 +2,7 @@ import { Joi } from '@arcblock/validator';
2
2
  import { Router } from 'express';
3
3
 
4
4
  import { BN } from '@ocap/util';
5
- import { createListParamSchema } from '../libs/api';
5
+ import { createListParamSchema, getOrder } from '../libs/api';
6
6
  import logger from '../libs/logger';
7
7
  import { CheckoutSession } from '../store/models/checkout-session';
8
8
  import { Customer } from '../store/models/customer';
@@ -143,7 +143,7 @@ router.get('/', async (req, res) => {
143
143
  'created_at',
144
144
  'updated_at',
145
145
  ],
146
- order: [['created_at', 'DESC']],
146
+ order: getOrder(req.query, [['created_at', 'DESC']]),
147
147
  offset: (page - 1) * pageSize,
148
148
  include: [{ model: Customer, as: 'customer', attributes: ['id', 'did', 'name', 'metadata'] }],
149
149
  limit: pageSize,
@@ -2,7 +2,7 @@ import { Router } from 'express';
2
2
  import Joi from 'joi';
3
3
  import type { WhereOptions } from 'sequelize';
4
4
 
5
- import { createListParamSchema } from '../libs/api';
5
+ import { createListParamSchema, getOrder } from '../libs/api';
6
6
  import { authenticate } from '../libs/security';
7
7
  import { Event } from '../store/models/event';
8
8
  import { blocklet } from '../libs/auth';
@@ -42,7 +42,7 @@ router.get('/', auth, async (req, res) => {
42
42
  const { rows: list, count } = await Event.findAndCountAll({
43
43
  where,
44
44
  attributes: { exclude: ['data', 'request'] },
45
- order: [['created_at', 'DESC']],
45
+ order: getOrder(req.query, [['created_at', 'DESC']]),
46
46
  offset: (page - 1) * pageSize,
47
47
  limit: pageSize,
48
48
  include: [],
@@ -8,7 +8,7 @@ import { Op } from 'sequelize';
8
8
  import { BN } from '@ocap/util';
9
9
  import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
10
10
  import { syncStripePayment } from '../integrations/stripe/handlers/payment-intent';
11
- import { createListParamSchema, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
11
+ import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
12
12
  import { authenticate } from '../libs/security';
13
13
  import { expandLineItems } from '../libs/session';
14
14
  import { formatMetadata, getBlockletJson, getUserOrAppInfo } from '../libs/util';
@@ -136,7 +136,7 @@ router.get('/', authMine, async (req, res) => {
136
136
  try {
137
137
  const { rows: list, count } = await Invoice.findAndCountAll({
138
138
  where,
139
- order: [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']],
139
+ order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
140
140
  offset: (page - 1) * pageSize,
141
141
  limit: pageSize,
142
142
  include: [
@@ -212,7 +212,7 @@ router.get('/recharge', authMine, async (req, res) => {
212
212
  },
213
213
  offset: (page - 1) * pageSize,
214
214
  limit: pageSize,
215
- order: [['created_at', 'DESC']],
215
+ order: getOrder(req.query, [['created_at', 'DESC']]),
216
216
  include: [
217
217
  { model: PaymentCurrency, as: 'paymentCurrency' },
218
218
  { model: PaymentMethod, as: 'paymentMethod' },
@@ -240,7 +240,7 @@ router.get('/search', authMine, async (req, res) => {
240
240
 
241
241
  const { rows: list, count } = await Invoice.findAndCountAll({
242
242
  where,
243
- order: [['created_at', o === 'asc' ? 'ASC' : 'DESC']],
243
+ order: getOrder(req.query, [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
244
244
  offset: (page - 1) * pageSize,
245
245
  limit: pageSize,
246
246
  distinct: true,
@@ -9,6 +9,7 @@ import { syncStripePayment } from '../integrations/stripe/handlers/payment-inten
9
9
  import {
10
10
  BNPositiveValidator,
11
11
  createListParamSchema,
12
+ getOrder,
12
13
  getWhereFromKvQuery,
13
14
  getWhereFromQuery,
14
15
  MetadataSchema,
@@ -111,7 +112,7 @@ router.get('/', authMine, async (req, res) => {
111
112
  try {
112
113
  const { rows: list, count } = await PaymentIntent.findAndCountAll({
113
114
  where,
114
- order: [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']],
115
+ order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
115
116
  offset: (page - 1) * pageSize,
116
117
  limit: pageSize,
117
118
  include: [
@@ -147,7 +148,7 @@ router.get('/search', authMine, async (req, res) => {
147
148
 
148
149
  const { rows: list, count } = await PaymentIntent.findAndCountAll({
149
150
  where,
150
- order: [['created_at', o === 'asc' ? 'ASC' : 'DESC']],
151
+ order: getOrder(req.query, [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
151
152
  offset: (page - 1) * pageSize,
152
153
  limit: pageSize,
153
154
  include: [
@@ -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, MetadataSchema } from '../libs/api';
7
+ import { createListParamSchema, getOrder, 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';
@@ -239,7 +239,7 @@ router.get('/', auth, async (req, res) => {
239
239
  try {
240
240
  const { rows: list, count } = await PaymentLink.findAndCountAll({
241
241
  where,
242
- order: [['created_at', 'DESC']],
242
+ order: getOrder(req.query, [['created_at', 'DESC']]),
243
243
  offset: (page - 1) * pageSize,
244
244
  limit: pageSize,
245
245
  include: [],
@@ -5,7 +5,7 @@ import { joinURL } from 'ufo';
5
5
 
6
6
  import { getPaymentStat } from '../crons/payment-stat';
7
7
  import { getTokenSummaryByDid } from '../integrations/arcblock/stake';
8
- import { createListParamSchema } from '../libs/api';
8
+ import { createListParamSchema, getOrder } from '../libs/api';
9
9
  import { ethWallet, wallet } from '../libs/auth';
10
10
  import dayjs from '../libs/dayjs';
11
11
  import { authenticate } from '../libs/security';
@@ -56,7 +56,7 @@ router.get('/', auth, async (req, res) => {
56
56
  try {
57
57
  const { rows: list, count } = await PaymentStat.findAndCountAll({
58
58
  where,
59
- order: [['created_at', 'ASC']],
59
+ order: getOrder(req.query, [['created_at', 'ASC']]),
60
60
  include: [],
61
61
  });
62
62
 
@@ -4,7 +4,7 @@ import Joi from 'joi';
4
4
  import pick from 'lodash/pick';
5
5
 
6
6
  import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
7
- import { createListParamSchema, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
7
+ import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
8
8
  import { authenticate } from '../libs/security';
9
9
  import { formatMetadata } from '../libs/util';
10
10
  import { Customer } from '../store/models/customer';
@@ -90,7 +90,7 @@ router.get('/', authMine, async (req, res) => {
90
90
  try {
91
91
  const { rows: list, count } = await Payout.findAndCountAll({
92
92
  where,
93
- order: [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']],
93
+ order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
94
94
  offset: (page - 1) * pageSize,
95
95
  limit: pageSize,
96
96
  include: [
@@ -146,7 +146,7 @@ router.get('/mine', sessionMiddleware(), async (req, res) => {
146
146
 
147
147
  const { rows: list, count } = await Payout.findAndCountAll({
148
148
  where,
149
- order: [['created_at', 'DESC']],
149
+ order: getOrder(req.query, [['created_at', 'DESC']]),
150
150
  offset: (page - 1) * pageSize,
151
151
  limit: pageSize,
152
152
  include: [
@@ -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, MetadataSchema } from '../libs/api';
7
+ import { createListParamSchema, getOrder, 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';
@@ -88,7 +88,7 @@ router.get('/', auth, async (req, res) => {
88
88
  const { rows, count } = await Price.findAndCountAll({
89
89
  where,
90
90
  attributes: ['id'],
91
- order: [['created_at', 'DESC']],
91
+ order: getOrder(req.query, [['created_at', 'DESC']]),
92
92
  offset: (page - 1) * pageSize,
93
93
  limit: pageSize,
94
94
  });
@@ -115,7 +115,7 @@ router.get('/search', auth, async (req, res) => {
115
115
  const { rows, count } = await Price.findAndCountAll({
116
116
  where,
117
117
  attributes: ['id'],
118
- order: [['created_at', 'DESC']],
118
+ order: getOrder(req.query, [['created_at', 'DESC']]),
119
119
  offset: (page - 1) * pageSize,
120
120
  limit: pageSize,
121
121
  });
@@ -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, MetadataSchema } from '../libs/api';
11
+ import { createListParamSchema, getOrder, 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';
@@ -107,7 +107,7 @@ router.get('/', auth, async (req, res) => {
107
107
  try {
108
108
  const { rows: list, count } = await PricingTable.findAndCountAll({
109
109
  where,
110
- order: [['created_at', 'DESC']],
110
+ order: getOrder(req.query, [['created_at', 'DESC']]),
111
111
  offset: (page - 1) * pageSize,
112
112
  limit: pageSize,
113
113
  include: [],
@@ -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, MetadataSchema } from '../libs/api';
8
+ import { createListParamSchema, getOrder, 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';
@@ -257,7 +257,7 @@ router.get('/', auth, async (req, res) => {
257
257
 
258
258
  const { rows: list, count } = await Product.findAndCountAll({
259
259
  where,
260
- order: [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']],
260
+ order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
261
261
  offset: (page - 1) * pageSize,
262
262
  limit: pageSize,
263
263
  include: [{ model: Price, as: 'prices' }],
@@ -286,7 +286,7 @@ router.get('/search', auth, async (req, res) => {
286
286
 
287
287
  const { rows: list, count } = await Product.findAndCountAll({
288
288
  where,
289
- order: [['created_at', 'DESC']],
289
+ order: getOrder(req.query, [['created_at', 'DESC']]),
290
290
  offset: (page - 1) * pageSize,
291
291
  limit: pageSize,
292
292
  include: [{ model: Price, as: 'prices', separate: true }],
@@ -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, MetadataSchema } from '../libs/api';
8
+ import { BNPositiveValidator, createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
9
9
  import { authenticate } from '../libs/security';
10
10
  import { formatMetadata } from '../libs/util';
11
11
  import {
@@ -94,7 +94,7 @@ router.get('/', auth, async (req, res) => {
94
94
 
95
95
  const { rows: list, count } = await Refund.findAndCountAll({
96
96
  where,
97
- order: [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']],
97
+ order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
98
98
  offset: (page - 1) * pageSize,
99
99
  limit: pageSize,
100
100
  include: [
@@ -196,7 +196,7 @@ router.get('/search', auth, async (req, res) => {
196
196
 
197
197
  const { rows: list, count } = await Refund.findAndCountAll({
198
198
  where,
199
- order: [['created_at', o === 'asc' ? 'ASC' : 'DESC']],
199
+ order: getOrder(req.query, [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
200
200
  offset: (page - 1) * pageSize,
201
201
  limit: pageSize,
202
202
  include: [
@@ -9,7 +9,7 @@ import { PaymentMethod } from '../store/models/payment-method';
9
9
  import { authenticate } from '../libs/security';
10
10
  import { Setting } from '../store/models';
11
11
  import logger from '../libs/logger';
12
- import { createListParamSchema, getWhereFromKvQuery } from '../libs/api';
12
+ import { createListParamSchema, getOrder, getWhereFromKvQuery } from '../libs/api';
13
13
 
14
14
  const router = Router();
15
15
  const authAdmin = authenticate<Setting>({ component: true, roles: ['owner', 'admin'] });
@@ -80,7 +80,7 @@ router.get('/donate', async (req, res) => {
80
80
 
81
81
  const { rows: list, count } = await Setting.findAndCountAll({
82
82
  where,
83
- order: [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']],
83
+ order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
84
84
  offset: (page - 1) * pageSize,
85
85
  limit: pageSize,
86
86
  distinct: true,
@@ -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, MetadataSchema } from '../libs/api';
7
+ import { createListParamSchema, getOrder, 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';
@@ -80,7 +80,7 @@ router.get('/', auth, async (req, res) => {
80
80
  try {
81
81
  const { rows, count } = await SubscriptionItem.findAndCountAll({
82
82
  where,
83
- order: [['created_at', 'DESC']],
83
+ order: getOrder(req.query, [['created_at', 'DESC']]),
84
84
  offset: (page - 1) * pageSize,
85
85
  limit: pageSize,
86
86
  include: [],
@@ -10,7 +10,7 @@ import { literal, Op, OrderItem } from 'sequelize';
10
10
  import { BN } from '@ocap/util';
11
11
  import { createEvent } from '../libs/audit';
12
12
  import { ensureStripeCustomer, ensureStripePrice, ensureStripeSubscription } from '../integrations/stripe/resource';
13
- import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
13
+ import { createListParamSchema, getOrder, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
14
14
  import dayjs from '../libs/dayjs';
15
15
  import logger from '../libs/logger';
16
16
  import { isDelegationSufficientForPayment } from '../libs/payment';
@@ -91,7 +91,6 @@ const schema = createListParamSchema<{
91
91
  customer_did?: string;
92
92
  activeFirst?: boolean;
93
93
  price_id?: string;
94
- order?: string | string[] | OrderItem | OrderItem[];
95
94
  showTotalCount?: boolean;
96
95
  }>({
97
96
  status: Joi.string().empty(''),
@@ -99,21 +98,9 @@ const schema = createListParamSchema<{
99
98
  customer_did: Joi.string().empty(''),
100
99
  activeFirst: Joi.boolean().optional(),
101
100
  price_id: Joi.string().empty(''),
102
- order: Joi.alternatives()
103
- .try(
104
- Joi.string(),
105
- Joi.array().items(Joi.string()),
106
- Joi.array().items(Joi.array().ordered(Joi.string(), Joi.string().valid('ASC', 'DESC').insensitive()))
107
- )
108
- .optional(),
109
101
  showTotalCount: Joi.boolean().optional(),
110
102
  });
111
103
 
112
- const parseOrder = (orderStr: string): OrderItem => {
113
- const [field, direction] = orderStr.split(':');
114
- return [field ?? '', (direction?.toUpperCase() as 'ASC' | 'DESC') || 'ASC'];
115
- };
116
-
117
104
  router.get('/', authMine, async (req, res) => {
118
105
  const { page, pageSize, status, livemode, ...query } = await schema.validateAsync(req.query, {
119
106
  stripUnknown: false,
@@ -139,9 +126,7 @@ router.get('/', authMine, async (req, res) => {
139
126
  return;
140
127
  }
141
128
  }
142
- if (typeof livemode === 'boolean') {
143
- where.livemode = livemode;
144
- }
129
+ where.livemode = typeof livemode === 'boolean' ? livemode : true;
145
130
 
146
131
  Object.keys(query)
147
132
  .filter((x) => x.startsWith('metadata.'))
@@ -150,11 +135,7 @@ router.get('/', authMine, async (req, res) => {
150
135
  where[key] = query[key];
151
136
  });
152
137
 
153
- let order: OrderItem[] = [];
154
- if (query.order) {
155
- const orderItems = Array.isArray(query.order) ? query.order : [query.order];
156
- order = orderItems.map((item) => (typeof item === 'string' ? parseOrder(item) : (item as OrderItem)));
157
- }
138
+ const order: OrderItem[] = getOrder(req.query, []);
158
139
 
159
140
  if (query.activeFirst) {
160
141
  order.unshift([
@@ -196,6 +177,7 @@ router.get('/', authMine, async (req, res) => {
196
177
  const totalCount = await Subscription.count({
197
178
  where: {
198
179
  customer_id: where.customer_id,
180
+ livemode: where.livemode ?? true,
199
181
  },
200
182
  distinct: true,
201
183
  });
@@ -228,7 +210,7 @@ router.get('/search', auth, async (req, res) => {
228
210
  // fix here https://github.com/blocklet/payment-kit/issues/394
229
211
  const { rows: list, count } = await Subscription.findAndCountAll({
230
212
  where,
231
- order: [['created_at', o === 'asc' ? 'ASC' : 'DESC']],
213
+ order: getOrder(req.query, [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
232
214
  offset: (page - 1) * pageSize,
233
215
  limit: pageSize,
234
216
  distinct: true,
@@ -1901,7 +1883,7 @@ router.get('/:id/recharge', authMine, async (req, res) => {
1901
1883
  },
1902
1884
  offset: (page - 1) * pageSize,
1903
1885
  limit: pageSize,
1904
- order: [['created_at', 'DESC']],
1886
+ order: getOrder(req.query, [['created_at', 'DESC']]),
1905
1887
  include: [
1906
1888
  { model: PaymentCurrency, as: 'paymentCurrency' },
1907
1889
  { model: PaymentMethod, as: 'paymentMethod' },
@@ -2036,15 +2018,11 @@ router.get('/:id/overdraft-protection', authPortal, async (req, res) => {
2036
2018
  }
2037
2019
  });
2038
2020
 
2039
- const overdraftProtectionSchema = createListParamSchema<{
2040
- amount: string;
2041
- enabled: boolean;
2042
- return_stake: boolean;
2043
- }>({
2021
+ const overdraftProtectionSchema = Joi.object({
2044
2022
  amount: Joi.number().empty(0).optional(),
2045
2023
  enabled: Joi.boolean().required(),
2046
2024
  return_stake: Joi.boolean().empty(false).optional(),
2047
- });
2025
+ }).unknown(true);
2048
2026
  // 订阅保护
2049
2027
  router.post('/:id/overdraft-protection', authPortal, async (req, res) => {
2050
2028
  try {
@@ -5,7 +5,7 @@ import pick from 'lodash/pick';
5
5
  import { Op } from 'sequelize';
6
6
 
7
7
  import { forwardUsageRecordToStripe } from '../integrations/stripe/resource';
8
- import { createListParamSchema } from '../libs/api';
8
+ import { createListParamSchema, getOrder } from '../libs/api';
9
9
  import dayjs from '../libs/dayjs';
10
10
  import { authenticate } from '../libs/security';
11
11
  import { usageRecordQueue } from '../queues/usage-record';
@@ -163,7 +163,7 @@ router.get('/summary', auth, async (req, res) => {
163
163
  const { rows, count } = await Invoice.findAndCountAll({
164
164
  where: { subscription_id: item.subscription_id },
165
165
  attributes: ['id', 'period_end', 'period_start'],
166
- order: [['created_at', 'DESC']],
166
+ order: getOrder(req.query, [['created_at', 'DESC']]),
167
167
  offset: (page - 1) * pageSize,
168
168
  limit: pageSize,
169
169
  });
@@ -2,7 +2,7 @@ import { Router } from 'express';
2
2
  import Joi from 'joi';
3
3
  import type { WhereOptions } from 'sequelize';
4
4
 
5
- import { createListParamSchema } from '../libs/api';
5
+ import { createListParamSchema, getOrder } from '../libs/api';
6
6
  import { authenticate } from '../libs/security';
7
7
  import { Event, WebhookAttempt, WebhookEndpoint } from '../store/models';
8
8
  import { blocklet } from '../libs/auth';
@@ -35,7 +35,7 @@ router.get('/', auth, async (req, res) => {
35
35
  try {
36
36
  const { rows: list, count } = await WebhookAttempt.findAndCountAll({
37
37
  where,
38
- order: [['created_at', 'DESC']],
38
+ order: getOrder(req.query, [['created_at', 'DESC']]),
39
39
  offset: (page - 1) * pageSize,
40
40
  limit: pageSize,
41
41
  include: [
@@ -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, MetadataSchema } from '../libs/api';
6
+ import { createListParamSchema, getOrder, 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';
@@ -62,7 +62,7 @@ router.get('/', auth, async (req, res) => {
62
62
  try {
63
63
  const { rows: list, count } = await WebhookEndpoint.findAndCountAll({
64
64
  where,
65
- order: [['created_at', 'DESC']],
65
+ order: getOrder(req.query, [['created_at', 'DESC']]),
66
66
  offset: (page - 1) * pageSize,
67
67
  limit: pageSize,
68
68
  include: [],
@@ -283,14 +283,14 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
283
283
  return nextInvoicePrefix();
284
284
  }
285
285
 
286
- public static formatAddressFromUser(user: any) {
286
+ public static formatAddressFromUser(user: any, customer?: Customer) {
287
287
  return {
288
- country: user.address?.country || '',
289
- state: user.address?.province || '',
290
- city: user.address?.city || '',
291
- line1: user.address?.line1 || '',
292
- line2: user.address?.line2 || '',
293
- postal_code: user.address?.postalCode || '',
288
+ country: user.address?.country || customer?.address?.country || '',
289
+ state: user.address?.province || customer?.address?.state || '',
290
+ city: user.address?.city || customer?.address?.city || '',
291
+ line1: user.address?.line1 || customer?.address?.line1 || '',
292
+ line2: user.address?.line2 || customer?.address?.line2 || '',
293
+ postal_code: user.address?.postalCode || customer?.address?.postal_code || '',
294
294
  };
295
295
  }
296
296
 
@@ -10,6 +10,10 @@ export type ChainType = 'arcblock' | 'bitcoin' | 'stripe' | EVMChainType;
10
10
  export type GroupedBN = { [currencyId: string]: string };
11
11
  export type GroupedStrList = { [currencyId: string]: string[] };
12
12
 
13
+ export type OrderDirection = 'ASC' | 'DESC';
14
+ export type OrderItem = [string, OrderDirection];
15
+ export type OrderInput = string | string[] | OrderItem[];
16
+
13
17
  export type Pagination<T = any> = T & {
14
18
  // offset based
15
19
  page?: number;
@@ -18,6 +22,7 @@ export type Pagination<T = any> = T & {
18
22
  // TODO: cursor based
19
23
  starting_after?: string;
20
24
  ending_before?: string;
25
+ order?: OrderInput; // order by field:ASC|DESC or [field:ASC|DESC, ...] or [[field, direction], ...]
21
26
  };
22
27
 
23
28
  export type Searchable<T = any> = Pagination<T> & {
@@ -1,6 +1,7 @@
1
1
  import { Op } from 'sequelize';
2
2
 
3
- import { getWhereFromQuery, MetadataSchema } from '../../src/libs/api';
3
+ import { getOrder, getWhereFromQuery, MetadataSchema, normalizeOrder, createListParamSchema } from '../../src/libs/api';
4
+ import type { OrderItem } from '../../src/store/models';
4
5
 
5
6
  describe('getWhereFromQuery', () => {
6
7
  it('should correctly parse > operator', () => {
@@ -224,3 +225,299 @@ describe('MetadataSchema', () => {
224
225
  expect(error).toBeUndefined();
225
226
  });
226
227
  });
228
+
229
+ describe('normalizeOrder', () => {
230
+ it('should return default order when input is undefined', () => {
231
+ const result = normalizeOrder(undefined);
232
+ expect(result).toEqual([['created_at', 'DESC']]);
233
+ });
234
+
235
+ it('should handle string input', () => {
236
+ const result = normalizeOrder('name:asc');
237
+ expect(result).toEqual([
238
+ ['name', 'ASC'],
239
+ ['created_at', 'DESC'],
240
+ ]);
241
+ });
242
+
243
+ it('should handle string array input', () => {
244
+ const result = normalizeOrder(['name:asc', 'age:desc']);
245
+ expect(result).toEqual([
246
+ ['name', 'ASC'],
247
+ ['age', 'DESC'],
248
+ ['created_at', 'DESC'],
249
+ ]);
250
+ });
251
+
252
+ it('should handle tuple array input', () => {
253
+ const result = normalizeOrder([
254
+ ['name', 'ASC'],
255
+ ['age', 'DESC'],
256
+ ]);
257
+ expect(result).toEqual([
258
+ ['name', 'ASC'],
259
+ ['age', 'DESC'],
260
+ ['created_at', 'DESC'],
261
+ ]);
262
+ });
263
+
264
+ it('should merge with default order correctly', () => {
265
+ const defaultOrder: OrderItem[] = [
266
+ ['updated_at', 'DESC'],
267
+ ['created_at', 'ASC'],
268
+ ];
269
+ const result = normalizeOrder('name:asc', defaultOrder);
270
+ expect(result).toEqual([
271
+ ['name', 'ASC'],
272
+ ['updated_at', 'DESC'],
273
+ ['created_at', 'ASC'],
274
+ ]);
275
+ });
276
+
277
+ it('should not duplicate fields from default order', () => {
278
+ const defaultOrder: OrderItem[] = [
279
+ ['name', 'DESC'],
280
+ ['created_at', 'ASC'],
281
+ ];
282
+ const result = normalizeOrder('name:asc', defaultOrder);
283
+ expect(result).toEqual([
284
+ ['name', 'ASC'],
285
+ ['created_at', 'ASC'],
286
+ ]);
287
+ });
288
+ });
289
+
290
+ describe('getOrder', () => {
291
+ it('should handle query.order parameter', () => {
292
+ const result = getOrder({ order: 'name:asc' });
293
+ expect(result).toEqual([
294
+ ['name', 'ASC'],
295
+ ['created_at', 'DESC'],
296
+ ]);
297
+ });
298
+
299
+ it('should handle query.o parameter', () => {
300
+ const result = getOrder({ o: 'desc' }, [
301
+ ['name', 'ASC'],
302
+ ['age', 'ASC'],
303
+ ]);
304
+ expect(result).toEqual([
305
+ ['name', 'DESC'],
306
+ ['age', 'DESC'],
307
+ ]);
308
+ });
309
+
310
+ it('should handle query.o parameter without defaultOrder', () => {
311
+ const result = getOrder({ o: 'asc' });
312
+ expect(result).toEqual([['created_at', 'ASC']]);
313
+ });
314
+
315
+ it('should prioritize query.order over query.o', () => {
316
+ const result = getOrder({ order: 'name:asc', o: 'desc' });
317
+ expect(result).toEqual([
318
+ ['name', 'ASC'],
319
+ ['created_at', 'DESC'],
320
+ ]);
321
+ });
322
+
323
+ it('should return default order when no parameters provided', () => {
324
+ const result = getOrder({});
325
+ expect(result).toEqual([['created_at', 'DESC']]);
326
+ });
327
+
328
+ it('should return custom default order when provided', () => {
329
+ const defaultOrder: OrderItem[] = [
330
+ ['updated_at', 'DESC'],
331
+ ['name', 'ASC'],
332
+ ];
333
+ const result = getOrder({}, defaultOrder);
334
+ expect(result).toEqual(defaultOrder);
335
+ });
336
+
337
+ it('should handle complex ordering with array input', () => {
338
+ const result = getOrder({ order: ['name:asc', 'age:desc'] });
339
+ expect(result).toEqual([
340
+ ['name', 'ASC'],
341
+ ['age', 'DESC'],
342
+ ['created_at', 'DESC'],
343
+ ]);
344
+ });
345
+
346
+ it('should handle tuple array input', () => {
347
+ const result = getOrder({
348
+ order: [
349
+ ['name', 'ASC'],
350
+ ['age', 'DESC'],
351
+ ],
352
+ });
353
+ expect(result).toEqual([
354
+ ['name', 'ASC'],
355
+ ['age', 'DESC'],
356
+ ['created_at', 'DESC'],
357
+ ]);
358
+ });
359
+
360
+ it('should merge order fields correctly with defaults', () => {
361
+ const defaultOrder: OrderItem[] = [
362
+ ['status', 'DESC'],
363
+ ['created_at', 'DESC'],
364
+ ];
365
+ const result = getOrder({ order: 'name:asc' }, defaultOrder);
366
+ expect(result).toEqual([
367
+ ['name', 'ASC'],
368
+ ['status', 'DESC'],
369
+ ['created_at', 'DESC'],
370
+ ]);
371
+ });
372
+ });
373
+
374
+ describe('createListParamSchema', () => {
375
+ const schema = createListParamSchema({});
376
+
377
+ describe('order validation', () => {
378
+ // Valid format validation
379
+ it('should validate valid string order format', () => {
380
+ const { error } = schema.validate({ order: 'name:asc' });
381
+ expect(error).toBeUndefined();
382
+ });
383
+
384
+ it('should validate valid array of strings order format', () => {
385
+ const { error } = schema.validate({ order: ['name:asc', 'age:desc'] });
386
+ expect(error).toBeUndefined();
387
+ });
388
+
389
+ it('should validate valid tuple array format', () => {
390
+ const { error } = schema.validate({
391
+ order: [
392
+ ['name', 'ASC'],
393
+ ['age', 'DESC'],
394
+ ],
395
+ });
396
+ expect(error).toBeUndefined();
397
+ });
398
+
399
+ // Invalid format validation
400
+ it('should reject invalid format with detailed error message', () => {
401
+ const { error } = schema.validate({ order: 'nameasc' });
402
+ expect(error).toBeDefined();
403
+ expect((error as any).details[0].message).toMatch(/Must use ":" to separate field and direction/);
404
+ });
405
+
406
+ // Field name validation
407
+ it('should reject invalid field characters with detailed error message', () => {
408
+ const invalidInputs = ['@name:asc', 'field-name:asc', 'field name:asc', ':asc'];
409
+
410
+ invalidInputs.forEach((input) => {
411
+ const { error } = schema.validate({ order: input });
412
+ expect(error).toBeDefined();
413
+ expect((error as any).details[0].message).toMatch(
414
+ /Field name can only contain letters, numbers, dots and underscores/
415
+ );
416
+ });
417
+ });
418
+
419
+ // Direction validation
420
+ it('should reject invalid direction with detailed error message', () => {
421
+ const invalidDirections = ['name:up', 'name:', 'name:invalid'];
422
+
423
+ invalidDirections.forEach((input) => {
424
+ const { error } = schema.validate({ order: input });
425
+ expect(error).toBeDefined();
426
+ expect((error as any).details[0].message).toMatch(/Direction must be either "asc" or "desc"/);
427
+ });
428
+ });
429
+
430
+ // Array format validation
431
+ it('should reject invalid array items with detailed error message', () => {
432
+ const invalidArrays = [['name:up'], ['@name:asc'], ['name:'], ['nameasc']];
433
+
434
+ invalidArrays.forEach((input) => {
435
+ const { error } = schema.validate({ order: input });
436
+ expect(error).toBeDefined();
437
+ });
438
+ });
439
+
440
+ // Tuple format validation
441
+ it('should reject invalid tuple format with detailed error message', () => {
442
+ const invalidTuples = [[['@name', 'ASC']], [['name', 'UP']], [['name']], [['name', 'ASC', 'extra']]];
443
+
444
+ invalidTuples.forEach((input, index) => {
445
+ const { error } = schema.validate({ order: input });
446
+ expect(error).toBeDefined();
447
+
448
+ if (index === 0) {
449
+ expect((error as any).details[0].message).toMatch(
450
+ /Field name can only contain letters, numbers, dots and underscores/
451
+ );
452
+ } else if (index === 1) {
453
+ expect((error as any).details[0].message).toMatch(/Direction must be either "asc" or "desc"/);
454
+ } else if (index === 2 || index === 3) {
455
+ // Test for array length validation
456
+ expect((error as any).details[0].message).toMatch(
457
+ /Each tuple must contain exactly 2 elements|Each tuple must contain \[field, direction\]/
458
+ );
459
+ }
460
+ });
461
+ });
462
+
463
+ // Case variation validation
464
+ it('should accept all valid case variations', () => {
465
+ const validInputs = [
466
+ 'name:asc',
467
+ 'name:ASC',
468
+ 'name:desc',
469
+ 'name:DESC',
470
+ ['name:asc'],
471
+ [['name', 'asc']],
472
+ [['name', 'ASC']],
473
+ [
474
+ ['name', 'ASC'],
475
+ ['age', 'desc'],
476
+ ],
477
+ ];
478
+
479
+ validInputs.forEach((input) => {
480
+ const { error } = schema.validate({ order: input });
481
+ expect(error).toBeUndefined();
482
+ });
483
+ });
484
+
485
+ // Testing specific error messages for each validation rule
486
+ it('should validate field name format', () => {
487
+ const { error } = schema.validate({ order: '@invalid:asc' });
488
+ expect(error).toBeDefined();
489
+ expect((error as any).details[0].message).toBe(
490
+ 'Field name can only contain letters, numbers, dots and underscores'
491
+ );
492
+ });
493
+
494
+ it('should validate separator', () => {
495
+ const { error } = schema.validate({ order: 'name-asc' });
496
+ expect(error).toBeDefined();
497
+ expect((error as any).details[0].message).toBe('Must use ":" to separate field and direction');
498
+ });
499
+
500
+ it('should validate direction', () => {
501
+ const { error } = schema.validate({ order: 'name:up' });
502
+ expect(error).toBeDefined();
503
+ expect((error as any).details[0].message).toBe('Direction must be either "asc" or "desc"');
504
+ });
505
+
506
+ it('should validate tuple format length', () => {
507
+ const { error } = schema.validate({
508
+ order: [['name', 'ASC', 'extra']],
509
+ });
510
+ expect(error).toBeDefined();
511
+ expect((error as any).details[0].message).toBe('Each tuple must contain exactly 2 elements');
512
+ });
513
+
514
+ // Object format should not be supported
515
+ it('should reject object format', () => {
516
+ const { error } = schema.validate({
517
+ order: { name: 'asc', age: 'desc' },
518
+ });
519
+ expect(error).toBeDefined();
520
+ expect((error as any).details[0].message).toMatch(/Invalid order format. Must be one of:/);
521
+ });
522
+ });
523
+ });
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.18.21
17
+ version: 1.18.23
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.18.21",
3
+ "version": "1.18.23",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -46,17 +46,17 @@
46
46
  "@abtnode/cron": "^1.16.40",
47
47
  "@arcblock/did": "^1.19.15",
48
48
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
49
- "@arcblock/did-connect": "^2.12.43",
49
+ "@arcblock/did-connect": "^2.12.44",
50
50
  "@arcblock/did-util": "^1.19.15",
51
51
  "@arcblock/jwt": "^1.19.15",
52
- "@arcblock/ux": "^2.12.43",
52
+ "@arcblock/ux": "^2.12.44",
53
53
  "@arcblock/validator": "^1.19.15",
54
54
  "@blocklet/js-sdk": "^1.16.40",
55
55
  "@blocklet/logger": "^1.16.40",
56
- "@blocklet/payment-react": "1.18.21",
56
+ "@blocklet/payment-react": "1.18.23",
57
57
  "@blocklet/sdk": "^1.16.40",
58
- "@blocklet/ui-react": "^2.12.43",
59
- "@blocklet/uploader": "^0.1.79",
58
+ "@blocklet/ui-react": "^2.12.44",
59
+ "@blocklet/uploader": "^0.1.80",
60
60
  "@blocklet/xss": "^0.1.30",
61
61
  "@mui/icons-material": "^5.16.6",
62
62
  "@mui/lab": "^5.0.0-alpha.173",
@@ -121,7 +121,7 @@
121
121
  "devDependencies": {
122
122
  "@abtnode/types": "^1.16.40",
123
123
  "@arcblock/eslint-config-ts": "^0.3.3",
124
- "@blocklet/payment-types": "1.18.21",
124
+ "@blocklet/payment-types": "1.18.23",
125
125
  "@types/cookie-parser": "^1.4.7",
126
126
  "@types/cors": "^2.8.17",
127
127
  "@types/debug": "^4.1.12",
@@ -167,5 +167,5 @@
167
167
  "parser": "typescript"
168
168
  }
169
169
  },
170
- "gitHead": "cf08902482dded20ee91402ed6d4678383965daf"
170
+ "gitHead": "4ffd236baa345ecd8facab1e71a6ed2491332fd4"
171
171
  }
package/src/libs/util.ts CHANGED
@@ -21,9 +21,8 @@ import { hexToNumber } from '@ocap/util';
21
21
  import { isEmpty, isObject } from 'lodash';
22
22
  import cloneDeep from 'lodash/cloneDeep';
23
23
  import isEqual from 'lodash/isEqual';
24
- import { joinURL, withQuery } from 'ufo';
24
+ import { joinURL } from 'ufo';
25
25
 
26
- import type { LiteralUnion } from 'type-fest';
27
26
  import { t } from '../locales/index';
28
27
 
29
28
  export const formatProductPrice = (
@@ -327,22 +326,6 @@ export function getInvoiceUsageReportStartEnd(invoice: TInvoiceExpanded, showPre
327
326
  return usageReportRange;
328
327
  }
329
328
 
330
- export function getCustomerProfileUrl({
331
- locale = 'en',
332
- userDid,
333
- }: {
334
- locale: LiteralUnion<'en' | 'zh', string>;
335
- userDid: string;
336
- }) {
337
- return joinURL(
338
- getPrefix(),
339
- withQuery('.well-known/service/user', {
340
- locale,
341
- did: userDid,
342
- })
343
- );
344
- }
345
-
346
329
  export function getAppInfo(address: string): { name: string; avatar: string; type: string; url: string } | null {
347
330
  const blockletJson = window.blocklet;
348
331
  if (blockletJson) {
@@ -12,6 +12,7 @@ import {
12
12
  formatTime,
13
13
  getCustomerAvatar,
14
14
  getPayoutStatusColor,
15
+ getUserProfileLink,
15
16
  useMobile,
16
17
  } from '@blocklet/payment-react';
17
18
  import type { TCustomer, TPayoutExpanded } from '@blocklet/payment-types';
@@ -30,7 +31,7 @@ import InfoRow from '../../../../components/info-row';
30
31
  import MetadataEditor from '../../../../components/metadata/editor';
31
32
  import MetadataList from '../../../../components/metadata/list';
32
33
  import SectionHeader from '../../../../components/section/header';
33
- import { getAppInfo, getCustomerProfileUrl, goBackOrFallback } from '../../../../libs/util';
34
+ import { getAppInfo, goBackOrFallback } from '../../../../libs/util';
34
35
  import InfoCard from '../../../../components/info-card';
35
36
  import InfoRowGroup from '../../../../components/info-row-group';
36
37
 
@@ -48,7 +49,7 @@ const InfoDirection = 'column';
48
49
  const InfoAlignItems = 'flex-start';
49
50
 
50
51
  export default function PayoutDetail(props: { id: string }) {
51
- const { t } = useLocaleContext();
52
+ const { t, locale } = useLocaleContext();
52
53
  const { isMobile } = useMobile();
53
54
  const [state, setState] = useSetState({
54
55
  adding: {
@@ -227,10 +228,7 @@ export default function PayoutDetail(props: { id: string }) {
227
228
  if (isAnonymousPayer) {
228
229
  return;
229
230
  }
230
- const url = getCustomerProfileUrl({
231
- userDid: paymentIntent?.customer?.did,
232
- locale: 'zh',
233
- });
231
+ const url = getUserProfileLink(paymentIntent?.customer?.did, locale);
234
232
  window.open(url, '_blank');
235
233
  }}>
236
234
  {paymentIntent?.customer?.name}
@@ -510,7 +510,7 @@ export default function CustomerHome() {
510
510
  <Box className="base-card section section-invoices">
511
511
  <Box className="section-header">
512
512
  <Typography variant="h3">{t('customer.invoiceHistory')}</Typography>
513
- {isEmpty(data?.summary?.due) !== false && (
513
+ {isEmpty(data?.summary?.due) === false && (
514
514
  <Tooltip title={t('payment.customer.pastDue.warning')}>
515
515
  <Button
516
516
  variant="text"
@@ -9,6 +9,7 @@ import {
9
9
  formatTime,
10
10
  getCustomerAvatar,
11
11
  getPayoutStatusColor,
12
+ getUserProfileLink,
12
13
  useMobile,
13
14
  } from '@blocklet/payment-react';
14
15
  import type { TCustomer, TPaymentLink, TPayoutExpanded } from '@blocklet/payment-types';
@@ -22,7 +23,7 @@ import DID from '@arcblock/ux/lib/DID';
22
23
  import InfoMetric from '../../../components/info-metric';
23
24
  import InfoRow from '../../../components/info-row';
24
25
  import SectionHeader from '../../../components/section/header';
25
- import { getCustomerProfileUrl, goBackOrFallback } from '../../../libs/util';
26
+ import { goBackOrFallback } from '../../../libs/util';
26
27
  import CustomerLink from '../../../components/customer/link';
27
28
  import InfoCard from '../../../components/info-card';
28
29
  import InfoRowGroup from '../../../components/info-row-group';
@@ -39,7 +40,7 @@ const fetchData = (
39
40
  };
40
41
 
41
42
  export default function PayoutDetail() {
42
- const { t } = useLocaleContext();
43
+ const { t, locale } = useLocaleContext();
43
44
  const { isMobile } = useMobile();
44
45
  const params = useParams<{ id: string }>();
45
46
  const { loading, error, data } = useRequest(() => fetchData(params.id!), {
@@ -178,10 +179,7 @@ export default function PayoutDetail() {
178
179
  if (isAnonymousPayer) {
179
180
  return;
180
181
  }
181
- const url = getCustomerProfileUrl({
182
- userDid: paymentIntent?.customer?.did,
183
- locale: 'zh',
184
- });
182
+ const url = getUserProfileLink(paymentIntent?.customer?.did, locale);
185
183
  window.open(url, '_blank');
186
184
  }}>
187
185
  {paymentIntent?.customer?.name}