payment-kit 1.13.141 → 1.13.142
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/libs/api.ts +22 -0
- package/api/src/routes/customers.ts +5 -3
- package/api/src/routes/invoices.ts +37 -0
- package/api/src/routes/payment-intents.ts +7 -4
- package/api/src/routes/products.ts +5 -3
- package/api/src/routes/subscriptions.ts +5 -3
- package/api/tests/libs/util.spec.ts +31 -0
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/components/invoice/list.tsx +58 -3
- package/src/components/payment-intent/list.tsx +68 -16
- package/src/components/subscription/description.tsx +1 -2
- package/src/components/subscription/items/usage-records.tsx +1 -1
- package/src/components/subscription/list.tsx +55 -3
- package/src/libs/util.ts +0 -8
- package/src/pages/admin/billing/subscriptions/detail.tsx +1 -2
- package/src/pages/admin/customers/customers/index.tsx +30 -3
- package/src/pages/admin/products/products/index.tsx +2 -0
- package/src/pages/customer/invoice.tsx +1 -1
- package/src/pages/customer/subscription/update.tsx +9 -3
package/api/src/libs/api.ts
CHANGED
|
@@ -70,3 +70,25 @@ export function getWhereFromQuery(query: string) {
|
|
|
70
70
|
const parsed = parser.parse(query);
|
|
71
71
|
return handleCondition(parsed);
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
export const getWhereFromKvQuery = (query: string) => {
|
|
75
|
+
const out: any = {};
|
|
76
|
+
const likes: any = [];
|
|
77
|
+
query.split(' ').forEach((kv: string) => {
|
|
78
|
+
const [k, v] = kv.split(':');
|
|
79
|
+
if (v) {
|
|
80
|
+
let value = decodeURIComponent(v).replace('+', ' ');
|
|
81
|
+
if ((k as any).indexOf('like') > -1) {
|
|
82
|
+
// like
|
|
83
|
+
value = { [k?.slice(5) as any]: { [Op.like]: `%${v}%` } } as any;
|
|
84
|
+
likes.push(value);
|
|
85
|
+
} else {
|
|
86
|
+
out[k as string] = value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
if (likes.length > 0) {
|
|
91
|
+
out[Op.or] = likes;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
};
|
|
@@ -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 { getWhereFromQuery } from '../libs/api';
|
|
7
|
+
import { getWhereFromKvQuery, getWhereFromQuery } 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';
|
|
@@ -65,19 +65,21 @@ const searchSchema = Joi.object<{
|
|
|
65
65
|
pageSize: number;
|
|
66
66
|
query: string;
|
|
67
67
|
livemode?: boolean;
|
|
68
|
+
q?: string;
|
|
68
69
|
}>({
|
|
69
70
|
page: Joi.number().integer().min(1).default(1),
|
|
70
71
|
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
71
72
|
query: Joi.string(),
|
|
72
73
|
livemode: Joi.boolean().empty(''),
|
|
74
|
+
q: Joi.string(),
|
|
73
75
|
});
|
|
74
76
|
router.get('/search', auth, async (req, res) => {
|
|
75
|
-
const { page, pageSize, query, livemode } = await searchSchema.validateAsync(req.query, {
|
|
77
|
+
const { page, pageSize, query, livemode, q } = await searchSchema.validateAsync(req.query, {
|
|
76
78
|
stripUnknown: false,
|
|
77
79
|
allowUnknown: true,
|
|
78
80
|
});
|
|
79
81
|
|
|
80
|
-
const where = getWhereFromQuery(query);
|
|
82
|
+
const where = q ? getWhereFromKvQuery(q) : getWhereFromQuery(query);
|
|
81
83
|
if (typeof livemode === 'boolean') {
|
|
82
84
|
where.livemode = livemode;
|
|
83
85
|
}
|
|
@@ -4,6 +4,7 @@ import Joi from 'joi';
|
|
|
4
4
|
import type { WhereOptions } from 'sequelize';
|
|
5
5
|
|
|
6
6
|
import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
|
|
7
|
+
import { getWhereFromKvQuery } from '../libs/api';
|
|
7
8
|
import { authenticate } from '../libs/security';
|
|
8
9
|
import { expandLineItems } from '../libs/session';
|
|
9
10
|
import { Customer } from '../store/models/customer';
|
|
@@ -106,6 +107,42 @@ router.get('/', authMine, async (req, res) => {
|
|
|
106
107
|
}
|
|
107
108
|
});
|
|
108
109
|
|
|
110
|
+
const searchSchema = Joi.object<{
|
|
111
|
+
page: number;
|
|
112
|
+
pageSize: number;
|
|
113
|
+
livemode?: boolean;
|
|
114
|
+
q: string;
|
|
115
|
+
}>({
|
|
116
|
+
page: Joi.number().integer().min(1).default(1),
|
|
117
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
118
|
+
livemode: Joi.boolean().empty(''),
|
|
119
|
+
q: Joi.string(),
|
|
120
|
+
});
|
|
121
|
+
router.get('/search', authMine, async (req, res) => {
|
|
122
|
+
const { page, pageSize, livemode, q } = await searchSchema.validateAsync(req.query, {
|
|
123
|
+
stripUnknown: false,
|
|
124
|
+
allowUnknown: true,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const where = getWhereFromKvQuery(q);
|
|
128
|
+
if (typeof livemode === 'boolean') {
|
|
129
|
+
where.livemode = livemode;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { rows: list, count } = await Invoice.findAndCountAll({
|
|
133
|
+
where,
|
|
134
|
+
order: [['created_at', 'DESC']],
|
|
135
|
+
offset: (page - 1) * pageSize,
|
|
136
|
+
limit: pageSize,
|
|
137
|
+
include: [
|
|
138
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
139
|
+
{ model: Customer, as: 'customer' },
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
res.json({ count, list });
|
|
144
|
+
});
|
|
145
|
+
|
|
109
146
|
router.get('/:id', authPortal, async (req, res) => {
|
|
110
147
|
try {
|
|
111
148
|
const doc = await Invoice.findOne({
|
|
@@ -4,7 +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
|
+
import { getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
|
|
8
8
|
import { authenticate } from '../libs/security';
|
|
9
9
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
10
10
|
import { Customer } from '../store/models/customer';
|
|
@@ -109,23 +109,26 @@ const searchSchema = Joi.object<{
|
|
|
109
109
|
pageSize: number;
|
|
110
110
|
query: string;
|
|
111
111
|
livemode?: boolean;
|
|
112
|
+
q?: string;
|
|
112
113
|
}>({
|
|
113
114
|
page: Joi.number().integer().min(1).default(1),
|
|
114
115
|
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
115
116
|
query: Joi.string(),
|
|
116
117
|
livemode: Joi.boolean().empty(''),
|
|
118
|
+
q: Joi.string(),
|
|
117
119
|
});
|
|
118
120
|
router.get('/search', authMine, async (req, res) => {
|
|
119
|
-
const { page, pageSize, query, livemode } = await searchSchema.validateAsync(req.query, {
|
|
121
|
+
const { page, pageSize, query, livemode, q } = await searchSchema.validateAsync(req.query, {
|
|
120
122
|
stripUnknown: false,
|
|
121
123
|
allowUnknown: true,
|
|
122
124
|
});
|
|
123
125
|
|
|
124
|
-
const where = getWhereFromQuery(query);
|
|
126
|
+
const where = q ? getWhereFromKvQuery(q) : getWhereFromQuery(query);
|
|
125
127
|
if (typeof livemode === 'boolean') {
|
|
126
128
|
where.livemode = livemode;
|
|
127
129
|
}
|
|
128
|
-
|
|
130
|
+
|
|
131
|
+
const { rows: list, count } = await PaymentIntent.findAndCountAll({
|
|
129
132
|
where,
|
|
130
133
|
order: [['created_at', 'DESC']],
|
|
131
134
|
offset: (page - 1) * pageSize,
|
|
@@ -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 { getWhereFromQuery } from '../libs/api';
|
|
7
|
+
import { getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
|
|
8
8
|
import logger from '../libs/logger';
|
|
9
9
|
import { authenticate } from '../libs/security';
|
|
10
10
|
import { formatMetadata } from '../libs/util';
|
|
@@ -167,19 +167,21 @@ const searchSchema = Joi.object<{
|
|
|
167
167
|
pageSize: number;
|
|
168
168
|
query: string;
|
|
169
169
|
livemode?: boolean;
|
|
170
|
+
q?: string;
|
|
170
171
|
}>({
|
|
171
172
|
page: Joi.number().integer().min(1).default(1),
|
|
172
173
|
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
173
174
|
query: Joi.string(),
|
|
174
175
|
livemode: Joi.boolean().empty(''),
|
|
176
|
+
q: Joi.string(),
|
|
175
177
|
});
|
|
176
178
|
router.get('/search', auth, async (req, res) => {
|
|
177
|
-
const { page, pageSize, query, livemode } = await searchSchema.validateAsync(req.query, {
|
|
179
|
+
const { page, pageSize, query, livemode, q } = await searchSchema.validateAsync(req.query, {
|
|
178
180
|
stripUnknown: false,
|
|
179
181
|
allowUnknown: true,
|
|
180
182
|
});
|
|
181
183
|
|
|
182
|
-
const where = getWhereFromQuery(query);
|
|
184
|
+
const where = q ? getWhereFromKvQuery(q) : getWhereFromQuery(query);
|
|
183
185
|
if (typeof livemode === 'boolean') {
|
|
184
186
|
where.livemode = livemode;
|
|
185
187
|
}
|
|
@@ -6,7 +6,7 @@ import pick from 'lodash/pick';
|
|
|
6
6
|
import uniq from 'lodash/uniq';
|
|
7
7
|
import type { WhereOptions } from 'sequelize';
|
|
8
8
|
|
|
9
|
-
import { getWhereFromQuery } from '../libs/api';
|
|
9
|
+
import { getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
|
|
10
10
|
import dayjs from '../libs/dayjs';
|
|
11
11
|
import logger from '../libs/logger';
|
|
12
12
|
import { isDelegationSufficientForPayment } from '../libs/payment';
|
|
@@ -143,19 +143,21 @@ const searchSchema = Joi.object<{
|
|
|
143
143
|
pageSize: number;
|
|
144
144
|
query: string;
|
|
145
145
|
livemode?: boolean;
|
|
146
|
+
q?: string;
|
|
146
147
|
}>({
|
|
147
148
|
page: Joi.number().integer().min(1).default(1),
|
|
148
149
|
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
149
150
|
query: Joi.string(),
|
|
150
151
|
livemode: Joi.boolean().empty(''),
|
|
152
|
+
q: Joi.string(),
|
|
151
153
|
});
|
|
152
154
|
router.get('/search', auth, async (req, res) => {
|
|
153
|
-
const { page, pageSize, query, livemode } = await searchSchema.validateAsync(req.query, {
|
|
155
|
+
const { page, pageSize, query, livemode, q } = await searchSchema.validateAsync(req.query, {
|
|
154
156
|
stripUnknown: false,
|
|
155
157
|
allowUnknown: true,
|
|
156
158
|
});
|
|
157
159
|
|
|
158
|
-
const where = getWhereFromQuery(query);
|
|
160
|
+
const where = q ? getWhereFromKvQuery(q) : getWhereFromQuery(query);
|
|
159
161
|
if (typeof livemode === 'boolean') {
|
|
160
162
|
where.livemode = livemode;
|
|
161
163
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import { Op } from 'sequelize';
|
|
1
2
|
import dayjs from '../../src/libs/dayjs';
|
|
2
3
|
import {
|
|
3
4
|
createCodeGenerator,
|
|
4
5
|
createIdGenerator,
|
|
5
6
|
formatMetadata,
|
|
6
7
|
getDataObjectFromQuery,
|
|
8
|
+
|
|
7
9
|
getNextRetry,
|
|
8
10
|
tryWithTimeout,
|
|
9
11
|
} from '../../src/libs/util';
|
|
10
12
|
|
|
13
|
+
import { getWhereFromKvQuery } from '../../src/libs/api'
|
|
14
|
+
|
|
11
15
|
describe('createIdGenerator', () => {
|
|
12
16
|
it('should return a function that generates an ID with the specified prefix and size', () => {
|
|
13
17
|
const generateId = createIdGenerator('test', 10);
|
|
@@ -175,3 +179,30 @@ describe('getDataObjectFromQuery', () => {
|
|
|
175
179
|
});
|
|
176
180
|
});
|
|
177
181
|
});
|
|
182
|
+
|
|
183
|
+
describe('getWhereFromKvQuery', () => {
|
|
184
|
+
it('should return an empty object when the q string is empty', () => {
|
|
185
|
+
const result = getWhereFromKvQuery('');
|
|
186
|
+
expect(result).toEqual({});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return a common object when the q string without like', () => {
|
|
190
|
+
const q = 'status:succeeded xxx:yyy';
|
|
191
|
+
const result = getWhereFromKvQuery(q);
|
|
192
|
+
expect(result).toEqual({
|
|
193
|
+
status: 'succeeded',
|
|
194
|
+
xxx: 'yyy',
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should return a likes object when the q string with like', () => {
|
|
199
|
+
const q = 'like-status:succ like-email:1533';
|
|
200
|
+
const result = getWhereFromKvQuery(q);
|
|
201
|
+
expect(result).toEqual({
|
|
202
|
+
[Op.or]: [
|
|
203
|
+
{ status: { [Op.like]: `%succ%` } },
|
|
204
|
+
{ email: { [Op.like]: `%1533%` } }
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.142",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "cross-env COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@arcblock/jwt": "^1.18.110",
|
|
51
51
|
"@arcblock/ux": "^2.9.29",
|
|
52
52
|
"@blocklet/logger": "1.16.23",
|
|
53
|
-
"@blocklet/payment-react": "1.13.
|
|
53
|
+
"@blocklet/payment-react": "1.13.142",
|
|
54
54
|
"@blocklet/sdk": "1.16.23",
|
|
55
55
|
"@blocklet/ui-react": "^2.9.29",
|
|
56
56
|
"@blocklet/uploader": "^0.0.73",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"devDependencies": {
|
|
111
111
|
"@abtnode/types": "1.16.23",
|
|
112
112
|
"@arcblock/eslint-config-ts": "^0.2.4",
|
|
113
|
-
"@blocklet/payment-types": "1.13.
|
|
113
|
+
"@blocklet/payment-types": "1.13.142",
|
|
114
114
|
"@types/cookie-parser": "^1.4.6",
|
|
115
115
|
"@types/cors": "^2.8.17",
|
|
116
116
|
"@types/dotenv-flow": "^3.3.3",
|
|
@@ -149,5 +149,5 @@
|
|
|
149
149
|
"parser": "typescript"
|
|
150
150
|
}
|
|
151
151
|
},
|
|
152
|
-
"gitHead": "
|
|
152
|
+
"gitHead": "99dfea2fee995b1a5a086675119585a8f23eed0a"
|
|
153
153
|
}
|
|
@@ -16,17 +16,26 @@ import InvoiceActions from './action';
|
|
|
16
16
|
const fetchData = (params: Record<string, any> = {}): Promise<{ list: TInvoiceExpanded[]; count: number }> => {
|
|
17
17
|
const search = new URLSearchParams();
|
|
18
18
|
Object.keys(params).forEach((key) => {
|
|
19
|
-
|
|
19
|
+
let v = params[key];
|
|
20
|
+
if (key === 'q') {
|
|
21
|
+
v = Object.entries(params[key])
|
|
22
|
+
.map((x) => x.join(':'))
|
|
23
|
+
.join(' ');
|
|
24
|
+
}
|
|
25
|
+
search.set(key, String(v));
|
|
20
26
|
});
|
|
21
|
-
return
|
|
27
|
+
return params.q
|
|
28
|
+
? api.get(`api/invoices/search?${search.toString()}`).then((res) => res.data)
|
|
29
|
+
: api.get(`/api/invoices?${search.toString()}`).then((res) => res.data);
|
|
22
30
|
};
|
|
23
31
|
|
|
24
32
|
type SearchProps = {
|
|
25
|
-
status
|
|
33
|
+
status?: string;
|
|
26
34
|
pageSize: number;
|
|
27
35
|
page: number;
|
|
28
36
|
customer_id?: string;
|
|
29
37
|
subscription_id?: string;
|
|
38
|
+
q?: any;
|
|
30
39
|
};
|
|
31
40
|
|
|
32
41
|
type ListProps = {
|
|
@@ -121,6 +130,7 @@ export default function InvoiceList({ customer_id, subscription_id, features, st
|
|
|
121
130
|
name: 'status',
|
|
122
131
|
width: 60,
|
|
123
132
|
options: {
|
|
133
|
+
filter: true,
|
|
124
134
|
customBodyRenderLite: (_: string, index: number) => {
|
|
125
135
|
const item = data.list[index] as TInvoiceExpanded;
|
|
126
136
|
return <Status label={item?.status} color={getInvoiceStatusColor(item?.status)} />;
|
|
@@ -135,6 +145,7 @@ export default function InvoiceList({ customer_id, subscription_id, features, st
|
|
|
135
145
|
label: t('common.description'),
|
|
136
146
|
name: 'description',
|
|
137
147
|
options: {
|
|
148
|
+
filter: true,
|
|
138
149
|
customBodyRenderLite: (_: string, index: number) => {
|
|
139
150
|
const item = data.list[index];
|
|
140
151
|
return item?.description || item?.id;
|
|
@@ -181,6 +192,7 @@ export default function InvoiceList({ customer_id, subscription_id, features, st
|
|
|
181
192
|
name: 'customer_id',
|
|
182
193
|
width: 60,
|
|
183
194
|
options: {
|
|
195
|
+
filter: true,
|
|
184
196
|
customBodyRenderLite: (_: string, index: number) => {
|
|
185
197
|
const item = data.list[index] as TInvoiceExpanded;
|
|
186
198
|
return <CustomerLink customer={item.customer} />;
|
|
@@ -218,6 +230,49 @@ export default function InvoiceList({ customer_id, subscription_id, features, st
|
|
|
218
230
|
navigate(`/admin/billing/${item.id}`);
|
|
219
231
|
}
|
|
220
232
|
},
|
|
233
|
+
onFilterChange: (key: any, filterList: any, type: any, index: number) => {
|
|
234
|
+
if (type === 'reset') {
|
|
235
|
+
setSearch({
|
|
236
|
+
status: '',
|
|
237
|
+
customer_id,
|
|
238
|
+
subscription_id,
|
|
239
|
+
pageSize: 100,
|
|
240
|
+
page: 1,
|
|
241
|
+
});
|
|
242
|
+
} else if (filterList && filterList[index]) {
|
|
243
|
+
// 这里暂时没有更好的取值方式
|
|
244
|
+
const value = filterList[index][0];
|
|
245
|
+
setSearch({
|
|
246
|
+
q: {
|
|
247
|
+
...search.q,
|
|
248
|
+
[key]: encodeURIComponent(value),
|
|
249
|
+
},
|
|
250
|
+
pageSize: 100,
|
|
251
|
+
page: 1,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
onSearchChange: (text: string) => {
|
|
256
|
+
if (text) {
|
|
257
|
+
setSearch({
|
|
258
|
+
q: {
|
|
259
|
+
'like-status': text,
|
|
260
|
+
'like-customer_email': text,
|
|
261
|
+
'like-description': text,
|
|
262
|
+
},
|
|
263
|
+
pageSize: 100,
|
|
264
|
+
page: 1,
|
|
265
|
+
});
|
|
266
|
+
} else {
|
|
267
|
+
setSearch({
|
|
268
|
+
status: '',
|
|
269
|
+
customer_id,
|
|
270
|
+
subscription_id,
|
|
271
|
+
pageSize: 100,
|
|
272
|
+
page: 1,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
},
|
|
221
276
|
}}
|
|
222
277
|
toolbar={features?.toolbar}
|
|
223
278
|
footer={features?.footer}
|
|
@@ -3,9 +3,8 @@ import { getDurableData } from '@arcblock/ux/lib/Datatable';
|
|
|
3
3
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
4
|
import { Status, api, formatTime, getPaymentIntentStatusColor } from '@blocklet/payment-react';
|
|
5
5
|
import type { TPaymentIntentExpanded } from '@blocklet/payment-types';
|
|
6
|
-
import {
|
|
6
|
+
import { CircularProgress, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
|
|
7
7
|
import { fromUnitToToken } from '@ocap/util';
|
|
8
|
-
import { useRequest } from 'ahooks';
|
|
9
8
|
import { useEffect, useState } from 'react';
|
|
10
9
|
import { useNavigate } from 'react-router-dom';
|
|
11
10
|
|
|
@@ -16,17 +15,26 @@ import PaymentIntentActions from './actions';
|
|
|
16
15
|
const fetchData = (params: Record<string, any> = {}): Promise<{ list: TPaymentIntentExpanded[]; count: number }> => {
|
|
17
16
|
const search = new URLSearchParams();
|
|
18
17
|
Object.keys(params).forEach((key) => {
|
|
19
|
-
|
|
18
|
+
let v = params[key];
|
|
19
|
+
if (key === 'q') {
|
|
20
|
+
v = Object.entries(params[key])
|
|
21
|
+
.map((x) => x.join(':'))
|
|
22
|
+
.join(' ');
|
|
23
|
+
}
|
|
24
|
+
search.set(key, String(v));
|
|
20
25
|
});
|
|
21
|
-
return
|
|
26
|
+
return params.q
|
|
27
|
+
? api.get(`api/payment-intents/search?${search.toString()}`).then((res) => res.data)
|
|
28
|
+
: api.get(`/api/payment-intents?${search.toString()}`).then((res) => res.data);
|
|
22
29
|
};
|
|
23
30
|
|
|
24
31
|
type SearchProps = {
|
|
25
|
-
status
|
|
32
|
+
status?: string;
|
|
26
33
|
pageSize: number;
|
|
27
34
|
page: number;
|
|
28
35
|
customer_id?: string;
|
|
29
36
|
invoice_id?: string;
|
|
37
|
+
q?: any;
|
|
30
38
|
};
|
|
31
39
|
|
|
32
40
|
type ListProps = {
|
|
@@ -74,16 +82,15 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
|
|
|
74
82
|
page: persisted.page ? persisted.page + 1 : 1,
|
|
75
83
|
});
|
|
76
84
|
|
|
77
|
-
const
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
refresh();
|
|
80
|
-
}, [search, refresh]);
|
|
85
|
+
const [data, setData] = useState({}) as any;
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
fetchData(search).then((res: any) => {
|
|
89
|
+
setData(res);
|
|
90
|
+
});
|
|
91
|
+
}, [search]);
|
|
85
92
|
|
|
86
|
-
if (
|
|
93
|
+
if (!data) {
|
|
87
94
|
return <CircularProgress />;
|
|
88
95
|
}
|
|
89
96
|
|
|
@@ -121,6 +128,7 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
|
|
|
121
128
|
label: t('common.description'),
|
|
122
129
|
name: 'description',
|
|
123
130
|
options: {
|
|
131
|
+
filter: true,
|
|
124
132
|
customBodyRenderLite: (_: string, index: number) => {
|
|
125
133
|
const item = data.list[index] as TPaymentIntentExpanded;
|
|
126
134
|
return item.description || item.id;
|
|
@@ -153,11 +161,12 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
|
|
|
153
161
|
label: t('common.customer'),
|
|
154
162
|
name: 'customer_id',
|
|
155
163
|
options: {
|
|
164
|
+
filter: true,
|
|
156
165
|
customBodyRenderLite: (_: string, index: number) => {
|
|
157
166
|
const item = data.list[index] as TPaymentIntentExpanded;
|
|
158
167
|
return <CustomerLink customer={item.customer} />;
|
|
159
168
|
},
|
|
160
|
-
},
|
|
169
|
+
} as any,
|
|
161
170
|
});
|
|
162
171
|
}
|
|
163
172
|
|
|
@@ -173,14 +182,57 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
|
|
|
173
182
|
<Table
|
|
174
183
|
durable={listKey}
|
|
175
184
|
durableKeys={['page', 'rowsPerPage']}
|
|
176
|
-
data={data.list}
|
|
185
|
+
data={data.list || []}
|
|
177
186
|
columns={columns}
|
|
178
|
-
loading={
|
|
187
|
+
loading={!data}
|
|
179
188
|
onChange={onTableChange}
|
|
180
189
|
options={{
|
|
181
190
|
count: data.count,
|
|
182
191
|
page: search.page - 1,
|
|
183
192
|
rowsPerPage: search.pageSize,
|
|
193
|
+
onFilterChange: (key: any, filterList: any, type: any, index: number) => {
|
|
194
|
+
if (type === 'reset') {
|
|
195
|
+
setSearch({
|
|
196
|
+
status: '',
|
|
197
|
+
customer_id,
|
|
198
|
+
invoice_id,
|
|
199
|
+
pageSize: 100,
|
|
200
|
+
page: 1,
|
|
201
|
+
});
|
|
202
|
+
} else if (filterList && filterList[index]) {
|
|
203
|
+
// 这里暂时没有更好的取值方式
|
|
204
|
+
const value = filterList[index][0];
|
|
205
|
+
setSearch({
|
|
206
|
+
q: {
|
|
207
|
+
...search.q,
|
|
208
|
+
[key]: encodeURIComponent(value),
|
|
209
|
+
},
|
|
210
|
+
pageSize: 100,
|
|
211
|
+
page: 1,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
onSearchChange: (text: string) => {
|
|
216
|
+
if (text) {
|
|
217
|
+
setSearch({
|
|
218
|
+
q: {
|
|
219
|
+
'like-status': text,
|
|
220
|
+
'like-receipt_email': text,
|
|
221
|
+
'like-description': text,
|
|
222
|
+
},
|
|
223
|
+
pageSize: 100,
|
|
224
|
+
page: 1,
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
setSearch({
|
|
228
|
+
status: '',
|
|
229
|
+
customer_id,
|
|
230
|
+
invoice_id,
|
|
231
|
+
pageSize: 100,
|
|
232
|
+
page: 1,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
},
|
|
184
236
|
onRowClick: (_: any, { dataIndex }: any) => {
|
|
185
237
|
const item = data.list[dataIndex] as TPaymentIntentExpanded;
|
|
186
238
|
navigate(`/admin/payments/${item.id}`);
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { formatSubscriptionProduct } from '@blocklet/payment-react';
|
|
1
2
|
import type { TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
2
3
|
import { InfoOutlined } from '@mui/icons-material';
|
|
3
4
|
import { Stack, Tooltip, Typography } from '@mui/material';
|
|
4
5
|
|
|
5
|
-
import { formatSubscriptionProduct } from '../../libs/util';
|
|
6
|
-
|
|
7
6
|
type Props = {
|
|
8
7
|
subscription: TSubscriptionExpanded;
|
|
9
8
|
variant?: 'body1' | 'h5';
|
|
@@ -48,7 +48,7 @@ export function UsageRecordDialog(props: { subscriptionId: string; id: string; o
|
|
|
48
48
|
options: {
|
|
49
49
|
customBodyRenderLite: (_: string, index: number) => {
|
|
50
50
|
const item = data.list[index] as TUsageRecord;
|
|
51
|
-
return formatToDatetime(item.
|
|
51
|
+
return formatToDatetime(item.timestamp);
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
54
|
},
|
|
@@ -17,16 +17,25 @@ import SubscriptionStatus from './status';
|
|
|
17
17
|
const fetchData = (params: Record<string, any> = {}): Promise<{ list: TSubscriptionExpanded[]; count: number }> => {
|
|
18
18
|
const search = new URLSearchParams();
|
|
19
19
|
Object.keys(params).forEach((key) => {
|
|
20
|
-
|
|
20
|
+
let v = params[key];
|
|
21
|
+
if (key === 'q') {
|
|
22
|
+
v = Object.entries(params[key])
|
|
23
|
+
.map((x) => x.join(':'))
|
|
24
|
+
.join(' ');
|
|
25
|
+
}
|
|
26
|
+
search.set(key, String(v));
|
|
21
27
|
});
|
|
22
|
-
return
|
|
28
|
+
return params.q
|
|
29
|
+
? api.get(`api/subscriptions/search?${search.toString()}`).then((res) => res.data)
|
|
30
|
+
: api.get(`/api/subscriptions?${search.toString()}`).then((res) => res.data);
|
|
23
31
|
};
|
|
24
32
|
|
|
25
33
|
type SearchProps = {
|
|
26
|
-
status
|
|
34
|
+
status?: string;
|
|
27
35
|
pageSize: number;
|
|
28
36
|
page: number;
|
|
29
37
|
customer_id?: string;
|
|
38
|
+
q?: any;
|
|
30
39
|
};
|
|
31
40
|
|
|
32
41
|
type ListProps = {
|
|
@@ -97,6 +106,7 @@ export default function SubscriptionList({ customer_id, features, status }: List
|
|
|
97
106
|
label: t('common.status'),
|
|
98
107
|
name: 'status',
|
|
99
108
|
options: {
|
|
109
|
+
filter: true,
|
|
100
110
|
customBodyRenderLite: (_: string, index: number) => {
|
|
101
111
|
const item = data.list[index] as TSubscriptionExpanded;
|
|
102
112
|
return <SubscriptionStatus subscription={item} />;
|
|
@@ -147,6 +157,7 @@ export default function SubscriptionList({ customer_id, features, status }: List
|
|
|
147
157
|
label: t('common.customer'),
|
|
148
158
|
name: 'customer_id',
|
|
149
159
|
options: {
|
|
160
|
+
filter: true,
|
|
150
161
|
// @ts-ignore
|
|
151
162
|
customBodyRenderLite: (_: string, index: number) => {
|
|
152
163
|
const item = data.list[index] as TSubscriptionExpanded;
|
|
@@ -180,6 +191,47 @@ export default function SubscriptionList({ customer_id, features, status }: List
|
|
|
180
191
|
const item = data.list[dataIndex] as TSubscriptionExpanded;
|
|
181
192
|
navigate(`/admin/billing/${item.id}`);
|
|
182
193
|
},
|
|
194
|
+
onFilterChange: (key: any, filterList: any, type: any, index: number) => {
|
|
195
|
+
if (type === 'reset') {
|
|
196
|
+
setSearch({
|
|
197
|
+
status: '',
|
|
198
|
+
customer_id,
|
|
199
|
+
pageSize: 100,
|
|
200
|
+
page: 1,
|
|
201
|
+
});
|
|
202
|
+
} else if (filterList && filterList[index]) {
|
|
203
|
+
// 这里暂时没有更好的取值方式
|
|
204
|
+
const value = filterList[index][0];
|
|
205
|
+
setSearch({
|
|
206
|
+
q: {
|
|
207
|
+
...search.q,
|
|
208
|
+
[key]: encodeURIComponent(value),
|
|
209
|
+
},
|
|
210
|
+
pageSize: 100,
|
|
211
|
+
page: 1,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
onSearchChange: (text: string) => {
|
|
216
|
+
if (text) {
|
|
217
|
+
setSearch({
|
|
218
|
+
q: {
|
|
219
|
+
'like-status': text,
|
|
220
|
+
// 'like-receipt_email': text,
|
|
221
|
+
'like-description': text,
|
|
222
|
+
},
|
|
223
|
+
pageSize: 100,
|
|
224
|
+
page: 1,
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
setSearch({
|
|
228
|
+
status: '',
|
|
229
|
+
customer_id,
|
|
230
|
+
pageSize: 100,
|
|
231
|
+
page: 1,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
},
|
|
183
235
|
}}
|
|
184
236
|
toolbar={features?.toolbar}
|
|
185
237
|
footer={features?.footer}
|
package/src/libs/util.ts
CHANGED
|
@@ -14,7 +14,6 @@ import type {
|
|
|
14
14
|
TPaymentMethodExpanded,
|
|
15
15
|
TPrice,
|
|
16
16
|
TProductExpanded,
|
|
17
|
-
TSubscriptionItemExpanded,
|
|
18
17
|
} from '@blocklet/payment-types';
|
|
19
18
|
import cloneDeep from 'lodash/cloneDeep';
|
|
20
19
|
import isEqual from 'lodash/isEqual';
|
|
@@ -156,13 +155,6 @@ export function isPriceAligned(list: LineItem[], products: TProductExpanded[], i
|
|
|
156
155
|
};
|
|
157
156
|
}
|
|
158
157
|
|
|
159
|
-
export function formatSubscriptionProduct(items: TSubscriptionItemExpanded[], maxLength = 2) {
|
|
160
|
-
const names = items.map((x) => x.price.product?.name).filter(Boolean);
|
|
161
|
-
return (
|
|
162
|
-
names.slice(0, maxLength).join(', ') + (names.length > maxLength ? ` and ${names.length - maxLength} more` : '')
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
158
|
export function groupPricingTableItems(items: any[]) {
|
|
167
159
|
const grouped: { [key: string]: any[] } = {};
|
|
168
160
|
items.forEach((x: any, index) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable react/no-unstable-nested-components */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
|
-
import { TxLink, api, formatError, formatTime } from '@blocklet/payment-react';
|
|
4
|
+
import { TxLink, api, formatError, formatSubscriptionProduct, formatTime } from '@blocklet/payment-react';
|
|
5
5
|
import type { TProduct, TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
6
6
|
import { ArrowBackOutlined, Edit } from '@mui/icons-material';
|
|
7
7
|
import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
|
|
@@ -21,7 +21,6 @@ import SubscriptionActions from '../../../../components/subscription/actions';
|
|
|
21
21
|
import SubscriptionItemList from '../../../../components/subscription/items';
|
|
22
22
|
import SubscriptionMetrics from '../../../../components/subscription/metrics';
|
|
23
23
|
import SubscriptionStatus from '../../../../components/subscription/status';
|
|
24
|
-
import { formatSubscriptionProduct } from '../../../../libs/util';
|
|
25
24
|
|
|
26
25
|
const fetchData = (id: string): Promise<TSubscriptionExpanded> => {
|
|
27
26
|
return api.get(`/api/subscriptions/${id}`).then((res) => res.data);
|
|
@@ -14,9 +14,17 @@ import Table from '../../../../components/table';
|
|
|
14
14
|
const fetchData = (params: Record<string, any> = {}): Promise<{ list: TCustomer[]; count: number }> => {
|
|
15
15
|
const search = new URLSearchParams();
|
|
16
16
|
Object.keys(params).forEach((key) => {
|
|
17
|
-
|
|
17
|
+
let v = params[key];
|
|
18
|
+
if (key === 'q') {
|
|
19
|
+
v = Object.entries(params[key])
|
|
20
|
+
.map((x) => x.join(':'))
|
|
21
|
+
.join(' ');
|
|
22
|
+
}
|
|
23
|
+
search.set(key, String(v));
|
|
18
24
|
});
|
|
19
|
-
return
|
|
25
|
+
return params.q
|
|
26
|
+
? api.get(`api/customers/search?${search.toString()}`).then((res) => res.data)
|
|
27
|
+
: api.get(`/api/customers?${search.toString()}`).then((res) => res.data);
|
|
20
28
|
};
|
|
21
29
|
|
|
22
30
|
export default function CustomersList() {
|
|
@@ -25,7 +33,7 @@ export default function CustomersList() {
|
|
|
25
33
|
|
|
26
34
|
const { t } = useLocaleContext();
|
|
27
35
|
const navigate = useNavigate();
|
|
28
|
-
const [search, setSearch] = useState<{ active
|
|
36
|
+
const [search, setSearch] = useState<{ active?: string; pageSize: number; page: number; q?: any }>({
|
|
29
37
|
active: '',
|
|
30
38
|
pageSize: persisted.rowsPerPage || 20,
|
|
31
39
|
page: persisted.page ? persisted.page + 1 : 1,
|
|
@@ -111,6 +119,25 @@ export default function CustomersList() {
|
|
|
111
119
|
const item = data.list[dataIndex] as TCustomer;
|
|
112
120
|
navigate(`/admin/customers/${item.id}`);
|
|
113
121
|
},
|
|
122
|
+
onSearchChange: (text: string) => {
|
|
123
|
+
if (text) {
|
|
124
|
+
setSearch({
|
|
125
|
+
q: {
|
|
126
|
+
'like-name': text,
|
|
127
|
+
'like-email': text,
|
|
128
|
+
},
|
|
129
|
+
pageSize: 100,
|
|
130
|
+
page: 1,
|
|
131
|
+
active: '',
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
setSearch({
|
|
135
|
+
active: '',
|
|
136
|
+
pageSize: 100,
|
|
137
|
+
page: 1,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
},
|
|
114
141
|
}}
|
|
115
142
|
loading={loading}
|
|
116
143
|
onChange={onTableChange}
|
|
@@ -55,6 +55,7 @@ export default function ProductsList() {
|
|
|
55
55
|
label: t('admin.product.name.label'),
|
|
56
56
|
name: 'name',
|
|
57
57
|
options: {
|
|
58
|
+
filter: true,
|
|
58
59
|
customBodyRenderLite: (_: string, index: number) => {
|
|
59
60
|
const product = data.list[index] as TProductExpanded;
|
|
60
61
|
return (
|
|
@@ -71,6 +72,7 @@ export default function ProductsList() {
|
|
|
71
72
|
label: t('common.status'),
|
|
72
73
|
name: 'active',
|
|
73
74
|
options: {
|
|
75
|
+
filter: true,
|
|
74
76
|
customBodyRenderLite: (_: string, index: number) => {
|
|
75
77
|
const item = data.list[index];
|
|
76
78
|
return <Status label={item?.active ? 'Active' : 'Archived'} color={item?.active ? 'success' : 'default'} />;
|
|
@@ -43,7 +43,7 @@ export default function CustomerHome() {
|
|
|
43
43
|
success: t('payment.customer.invoice.paySuccess'),
|
|
44
44
|
error: t('payment.customer.invoice.payError'),
|
|
45
45
|
confirm: '',
|
|
46
|
-
},
|
|
46
|
+
} as any,
|
|
47
47
|
extraParams: { invoiceId: params.id },
|
|
48
48
|
onSuccess: async () => {
|
|
49
49
|
connectApi.close();
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/* eslint-disable react/no-unstable-nested-components */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
PricingTable,
|
|
6
|
+
api,
|
|
7
|
+
formatError,
|
|
8
|
+
formatPrice,
|
|
9
|
+
formatSubscriptionProduct,
|
|
10
|
+
formatTime,
|
|
11
|
+
} from '@blocklet/payment-react';
|
|
5
12
|
import type { TLineItemExpanded, TPricingTableExpanded, TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
6
13
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
7
14
|
import { LoadingButton } from '@mui/lab';
|
|
@@ -14,7 +21,6 @@ import { Link, useNavigate, useParams } from 'react-router-dom';
|
|
|
14
21
|
import InfoCard from '../../../components/info-card';
|
|
15
22
|
import SectionHeader from '../../../components/section/header';
|
|
16
23
|
import { useSessionContext } from '../../../contexts/session';
|
|
17
|
-
import { formatSubscriptionProduct } from '../../../libs/util';
|
|
18
24
|
|
|
19
25
|
const fetchData = async (
|
|
20
26
|
id: string
|
|
@@ -151,7 +157,7 @@ export default function CustomerSubscriptionUpdate() {
|
|
|
151
157
|
success: t('payment.customer.upgrade.success'),
|
|
152
158
|
error: t('payment.customer.upgrade.error'),
|
|
153
159
|
confirm: '',
|
|
154
|
-
},
|
|
160
|
+
} as any,
|
|
155
161
|
extraParams: { invoiceId: result.data.latest_invoice_id, subscriptionId: result.data.id },
|
|
156
162
|
onSuccess: () => {
|
|
157
163
|
setState({ paid: true, paying: false });
|