payment-kit 1.18.45 → 1.18.47
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/pagination.ts +403 -0
- package/api/src/routes/invoices.ts +295 -103
- package/api/tests/libs/pagination.spec.ts +549 -0
- package/blocklet.yml +1 -1
- package/package.json +10 -10
- package/src/components/subscription/portal/list.tsx +36 -26
- package/src/libs/util.ts +11 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +2 -10
- package/src/pages/customer/invoice/past-due.tsx +1 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
// This helps generate pagination test cases
|
|
2
|
+
|
|
3
|
+
import { mergePaginate, defaultTimeOrderBy, DataSource } from '../../src/libs/pagination';
|
|
4
|
+
|
|
5
|
+
describe('mergePaginate', () => {
|
|
6
|
+
// Create test data source
|
|
7
|
+
const createTestDataSource = (items: any[]): DataSource<any> => ({
|
|
8
|
+
count: () => Promise.resolve(items.length),
|
|
9
|
+
fetch: (limit: number, offset: number = 0) => Promise.resolve(items.slice(offset, offset + limit)),
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'database',
|
|
12
|
+
cacheable: false,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Create test data source for sorting tests (cached type to enable in-memory sorting)
|
|
17
|
+
const createSortingTestDataSource = (items: any[]): DataSource<any> => ({
|
|
18
|
+
count: () => Promise.resolve(items.length),
|
|
19
|
+
fetch: (limit: number, offset: number = 0) => Promise.resolve(items.slice(offset, offset + limit)),
|
|
20
|
+
meta: {
|
|
21
|
+
type: 'cached',
|
|
22
|
+
cacheable: true,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Create pre-sorted database data source (simulates database with ORDER BY)
|
|
27
|
+
const createSortedDatabaseDataSource = (items: any[], orderBy: (a: any, b: any) => number): DataSource<any> => {
|
|
28
|
+
const sortedItems = [...items].sort(orderBy);
|
|
29
|
+
return {
|
|
30
|
+
count: () => Promise.resolve(sortedItems.length),
|
|
31
|
+
fetch: (limit: number, offset: number = 0) => Promise.resolve(sortedItems.slice(offset, offset + limit)),
|
|
32
|
+
meta: {
|
|
33
|
+
type: 'database',
|
|
34
|
+
cacheable: false,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Create test items
|
|
40
|
+
const createTestItems = (count: number, baseDate: Date = new Date()) => {
|
|
41
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
42
|
+
id: `id_${i}`,
|
|
43
|
+
created_at: new Date(baseDate.getTime() + i * 1000).toISOString(),
|
|
44
|
+
value: i,
|
|
45
|
+
}));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe('Single Data Source', () => {
|
|
49
|
+
it('should handle empty data source', async () => {
|
|
50
|
+
const source = createTestDataSource([]);
|
|
51
|
+
const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy());
|
|
52
|
+
|
|
53
|
+
expect(result).toEqual({
|
|
54
|
+
total: 0,
|
|
55
|
+
data: [],
|
|
56
|
+
paging: {
|
|
57
|
+
page: 1,
|
|
58
|
+
pageSize: 10,
|
|
59
|
+
totalPages: 0,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle single data source with pagination', async () => {
|
|
65
|
+
const items = createTestItems(25);
|
|
66
|
+
const source = createTestDataSource(items);
|
|
67
|
+
|
|
68
|
+
const result = await mergePaginate([source], { page: 2, pageSize: 10 }, defaultTimeOrderBy());
|
|
69
|
+
|
|
70
|
+
expect(result.total).toBe(25);
|
|
71
|
+
expect(result.data).toHaveLength(10);
|
|
72
|
+
expect(result.paging).toEqual({
|
|
73
|
+
page: 2,
|
|
74
|
+
pageSize: 10,
|
|
75
|
+
totalPages: 3,
|
|
76
|
+
});
|
|
77
|
+
expect(result.data[0].value).toBe(10); // First item of second page
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle single data source with empty page', async () => {
|
|
81
|
+
const items = createTestItems(5);
|
|
82
|
+
const source = createTestDataSource(items);
|
|
83
|
+
|
|
84
|
+
const result = await mergePaginate([source], { page: 2, pageSize: 10 }, defaultTimeOrderBy());
|
|
85
|
+
|
|
86
|
+
expect(result.total).toBe(5);
|
|
87
|
+
expect(result.data).toHaveLength(0);
|
|
88
|
+
expect(result.paging).toEqual({
|
|
89
|
+
page: 2,
|
|
90
|
+
pageSize: 10,
|
|
91
|
+
totalPages: 1,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle page size larger than total items', async () => {
|
|
96
|
+
const items = createTestItems(5);
|
|
97
|
+
const source = createTestDataSource(items);
|
|
98
|
+
|
|
99
|
+
const result = await mergePaginate([source], { page: 1, pageSize: 20 }, defaultTimeOrderBy());
|
|
100
|
+
|
|
101
|
+
expect(result.total).toBe(5);
|
|
102
|
+
expect(result.data).toHaveLength(5);
|
|
103
|
+
expect(result.paging).toEqual({
|
|
104
|
+
page: 1,
|
|
105
|
+
pageSize: 20,
|
|
106
|
+
totalPages: 1,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle page number out of range', async () => {
|
|
111
|
+
const items = createTestItems(15);
|
|
112
|
+
const source = createTestDataSource(items);
|
|
113
|
+
|
|
114
|
+
// Page number out of range
|
|
115
|
+
const result1 = await mergePaginate([source], { page: 0, pageSize: 10 }, defaultTimeOrderBy());
|
|
116
|
+
expect(result1.paging.page).toBe(1); // Should be corrected to first page
|
|
117
|
+
expect(result1.data).toHaveLength(10);
|
|
118
|
+
|
|
119
|
+
// Page number too large
|
|
120
|
+
const result2 = await mergePaginate([source], { page: 100, pageSize: 10 }, defaultTimeOrderBy());
|
|
121
|
+
expect(result2.paging.page).toBe(100);
|
|
122
|
+
expect(result2.data).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle page size of 0', async () => {
|
|
126
|
+
const items = createTestItems(10);
|
|
127
|
+
const source = createTestDataSource(items);
|
|
128
|
+
|
|
129
|
+
const result = await mergePaginate([source], { page: 1, pageSize: 0 }, defaultTimeOrderBy());
|
|
130
|
+
|
|
131
|
+
expect(result.total).toBe(10);
|
|
132
|
+
expect(result.data).toHaveLength(10);
|
|
133
|
+
expect(result.paging).toEqual({
|
|
134
|
+
page: 1,
|
|
135
|
+
pageSize: 0,
|
|
136
|
+
totalPages: 1,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should return all data when pageSize is 0', async () => {
|
|
141
|
+
const items = createTestItems(10);
|
|
142
|
+
const source = createTestDataSource(items);
|
|
143
|
+
|
|
144
|
+
const result = await mergePaginate([source], { page: 1, pageSize: 0 }, defaultTimeOrderBy());
|
|
145
|
+
|
|
146
|
+
expect(result.total).toBe(10);
|
|
147
|
+
expect(result.data).toHaveLength(10);
|
|
148
|
+
expect(result.paging).toEqual({
|
|
149
|
+
page: 1,
|
|
150
|
+
pageSize: 0,
|
|
151
|
+
totalPages: 1,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return all data when pageSize is undefined', async () => {
|
|
156
|
+
const items = createTestItems(10);
|
|
157
|
+
const source = createTestDataSource(items);
|
|
158
|
+
|
|
159
|
+
const result = await mergePaginate([source], { page: 1, pageSize: undefined as any }, defaultTimeOrderBy());
|
|
160
|
+
|
|
161
|
+
expect(result.total).toBe(10);
|
|
162
|
+
expect(result.data).toHaveLength(10);
|
|
163
|
+
expect(result.paging).toEqual({
|
|
164
|
+
page: 1,
|
|
165
|
+
pageSize: 0,
|
|
166
|
+
totalPages: 1,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should return all data from multiple sources when pageSize is 0', async () => {
|
|
171
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
172
|
+
const source1 = createTestDataSource(createTestItems(5, baseDate));
|
|
173
|
+
const source2 = createTestDataSource(createTestItems(5, new Date(baseDate.getTime() + 5000)));
|
|
174
|
+
|
|
175
|
+
const result = await mergePaginate([source1, source2], { page: 1, pageSize: 0 }, defaultTimeOrderBy());
|
|
176
|
+
|
|
177
|
+
expect(result.total).toBe(10);
|
|
178
|
+
expect(result.data).toHaveLength(10);
|
|
179
|
+
expect(result.paging).toEqual({
|
|
180
|
+
page: 1,
|
|
181
|
+
pageSize: 0,
|
|
182
|
+
totalPages: 1,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('Multiple Data Sources', () => {
|
|
188
|
+
it('should merge multiple data sources correctly', async () => {
|
|
189
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
190
|
+
const items1 = [
|
|
191
|
+
{ id: 'id_1', created_at: new Date(baseDate.getTime() + 1000).toISOString(), value: 1 },
|
|
192
|
+
{ id: 'id_2', created_at: new Date(baseDate.getTime() + 2000).toISOString(), value: 2 },
|
|
193
|
+
];
|
|
194
|
+
const items2 = [
|
|
195
|
+
{ id: 'id_3', created_at: new Date(baseDate.getTime() + 3000).toISOString(), value: 3 },
|
|
196
|
+
{ id: 'id_4', created_at: new Date(baseDate.getTime() + 4000).toISOString(), value: 4 },
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
// Create pre-sorted database sources (simulating ORDER BY in database)
|
|
200
|
+
const orderBy = defaultTimeOrderBy('desc');
|
|
201
|
+
const source1 = createSortedDatabaseDataSource(items1, orderBy);
|
|
202
|
+
const source2 = createSortedDatabaseDataSource(items2, orderBy);
|
|
203
|
+
|
|
204
|
+
const result = await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, orderBy);
|
|
205
|
+
|
|
206
|
+
expect(result.total).toBe(4);
|
|
207
|
+
expect(result.data).toHaveLength(4);
|
|
208
|
+
expect(result.data[0].value).toBe(4); // Latest item should be first
|
|
209
|
+
expect(result.data[3].value).toBe(1); // Oldest item should be last
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should handle pagination across multiple sources', async () => {
|
|
213
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
214
|
+
const items1 = createTestItems(5, baseDate);
|
|
215
|
+
const items2 = createTestItems(5, new Date(baseDate.getTime() + 5000));
|
|
216
|
+
|
|
217
|
+
// For pagination test, we can use default database sources since we're testing pagination logic
|
|
218
|
+
const source1 = createTestDataSource(items1);
|
|
219
|
+
const source2 = createTestDataSource(items2);
|
|
220
|
+
|
|
221
|
+
const result = await mergePaginate([source1, source2], { page: 2, pageSize: 3 }, defaultTimeOrderBy());
|
|
222
|
+
|
|
223
|
+
expect(result.total).toBe(10);
|
|
224
|
+
expect(result.data).toHaveLength(3);
|
|
225
|
+
expect(result.paging).toEqual({
|
|
226
|
+
page: 2,
|
|
227
|
+
pageSize: 3,
|
|
228
|
+
totalPages: 4,
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should handle sources with different types', async () => {
|
|
233
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
234
|
+
const source1 = createTestDataSource(createTestItems(5, baseDate));
|
|
235
|
+
const source2: DataSource<any> = {
|
|
236
|
+
count: () => Promise.resolve(3),
|
|
237
|
+
fetch: (limit: number, offset: number = 0) =>
|
|
238
|
+
Promise.resolve(createTestItems(3, new Date(baseDate.getTime() + 10000)).slice(offset, offset + limit)),
|
|
239
|
+
meta: {
|
|
240
|
+
type: 'computed',
|
|
241
|
+
estimatedSize: 3,
|
|
242
|
+
cacheable: true,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const result = await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, defaultTimeOrderBy());
|
|
247
|
+
|
|
248
|
+
expect(result.total).toBe(8);
|
|
249
|
+
expect(result.data).toHaveLength(8);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should handle error in data source gracefully', async () => {
|
|
253
|
+
const source1 = createTestDataSource(createTestItems(5));
|
|
254
|
+
const source2: DataSource<any> = {
|
|
255
|
+
count: () => Promise.reject(new Error('Count error')),
|
|
256
|
+
fetch: () => Promise.reject(new Error('Fetch error')),
|
|
257
|
+
meta: {
|
|
258
|
+
type: 'database',
|
|
259
|
+
cacheable: false,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const result = await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, defaultTimeOrderBy());
|
|
264
|
+
|
|
265
|
+
expect(result.total).toBe(5);
|
|
266
|
+
expect(result.data).toHaveLength(5);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should handle empty data sources in the middle', async () => {
|
|
270
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
271
|
+
const source1 = createTestDataSource(createTestItems(5, baseDate));
|
|
272
|
+
const source2 = createTestDataSource([]);
|
|
273
|
+
const source3 = createTestDataSource(createTestItems(5, new Date(baseDate.getTime() + 5000)));
|
|
274
|
+
|
|
275
|
+
const result = await mergePaginate([source1, source2, source3], { page: 1, pageSize: 10 }, defaultTimeOrderBy());
|
|
276
|
+
|
|
277
|
+
expect(result.total).toBe(10);
|
|
278
|
+
expect(result.data).toHaveLength(10);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should handle multiple data sources with overlapping timestamps', async () => {
|
|
282
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
283
|
+
const items1 = [
|
|
284
|
+
{ id: 'id_1', created_at: new Date(baseDate.getTime() + 1000).toISOString(), value: 1 },
|
|
285
|
+
{ id: 'id_2', created_at: new Date(baseDate.getTime() + 3000).toISOString(), value: 2 },
|
|
286
|
+
];
|
|
287
|
+
const items2 = [
|
|
288
|
+
{ id: 'id_3', created_at: new Date(baseDate.getTime() + 2000).toISOString(), value: 3 },
|
|
289
|
+
{ id: 'id_4', created_at: new Date(baseDate.getTime() + 4000).toISOString(), value: 4 },
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
// Create pre-sorted database sources
|
|
293
|
+
const orderBy = defaultTimeOrderBy('desc');
|
|
294
|
+
const source1 = createSortedDatabaseDataSource(items1, orderBy);
|
|
295
|
+
const source2 = createSortedDatabaseDataSource(items2, orderBy);
|
|
296
|
+
|
|
297
|
+
const result = await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, orderBy);
|
|
298
|
+
|
|
299
|
+
// Verify sorting order: First by timestamp descending, then by ID descending
|
|
300
|
+
expect(result.data.map((item) => item.id)).toEqual(['id_4', 'id_2', 'id_3', 'id_1']);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should handle multiple data sources with late pages correctly', async () => {
|
|
304
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
305
|
+
|
|
306
|
+
// Create 3 data sources, each with 20 items (total 60 items)
|
|
307
|
+
const source1 = createTestDataSource(createTestItems(20, baseDate));
|
|
308
|
+
const source2 = createTestDataSource(createTestItems(20, new Date(baseDate.getTime() + 20000)));
|
|
309
|
+
const source3 = createTestDataSource(createTestItems(20, new Date(baseDate.getTime() + 40000)));
|
|
310
|
+
|
|
311
|
+
// Test page 4 (should have items 31-40 out of 60 total)
|
|
312
|
+
const result = await mergePaginate(
|
|
313
|
+
[source1, source2, source3],
|
|
314
|
+
{ page: 4, pageSize: 10 },
|
|
315
|
+
defaultTimeOrderBy('desc')
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(result.total).toBe(60);
|
|
319
|
+
expect(result.data).toHaveLength(10);
|
|
320
|
+
expect(result.paging).toEqual({
|
|
321
|
+
page: 4,
|
|
322
|
+
pageSize: 10,
|
|
323
|
+
totalPages: 6,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Test the last page (page 6, should have items 51-60)
|
|
327
|
+
const lastPageResult = await mergePaginate(
|
|
328
|
+
[source1, source2, source3],
|
|
329
|
+
{ page: 6, pageSize: 10 },
|
|
330
|
+
defaultTimeOrderBy('desc')
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
expect(lastPageResult.total).toBe(60);
|
|
334
|
+
expect(lastPageResult.data).toHaveLength(10);
|
|
335
|
+
expect(lastPageResult.paging).toEqual({
|
|
336
|
+
page: 6,
|
|
337
|
+
pageSize: 10,
|
|
338
|
+
totalPages: 6,
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('Sorting', () => {
|
|
344
|
+
it('should sort items in ascending order', async () => {
|
|
345
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
346
|
+
const items1 = [
|
|
347
|
+
{ id: 'id_1', created_at: new Date(baseDate.getTime() + 1000).toISOString(), value: 1 },
|
|
348
|
+
{ id: 'id_2', created_at: new Date(baseDate.getTime() + 2000).toISOString(), value: 2 },
|
|
349
|
+
];
|
|
350
|
+
const items2 = [
|
|
351
|
+
{ id: 'id_3', created_at: new Date(baseDate.getTime() + 3000).toISOString(), value: 3 },
|
|
352
|
+
{ id: 'id_4', created_at: new Date(baseDate.getTime() + 4000).toISOString(), value: 4 },
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
// Create pre-sorted database sources
|
|
356
|
+
const orderBy = defaultTimeOrderBy('asc');
|
|
357
|
+
const source1 = createSortedDatabaseDataSource(items1, orderBy);
|
|
358
|
+
const source2 = createSortedDatabaseDataSource(items2, orderBy);
|
|
359
|
+
|
|
360
|
+
const result = await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, orderBy);
|
|
361
|
+
|
|
362
|
+
expect(result.data[0].value).toBe(1); // Oldest item should be first
|
|
363
|
+
expect(result.data[3].value).toBe(4); // Latest item should be last
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should sort items in descending order', async () => {
|
|
367
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
368
|
+
const items1 = [
|
|
369
|
+
{ id: 'id_1', created_at: new Date(baseDate.getTime() + 1000).toISOString(), value: 1 },
|
|
370
|
+
{ id: 'id_2', created_at: new Date(baseDate.getTime() + 2000).toISOString(), value: 2 },
|
|
371
|
+
];
|
|
372
|
+
const items2 = [
|
|
373
|
+
{ id: 'id_3', created_at: new Date(baseDate.getTime() + 3000).toISOString(), value: 3 },
|
|
374
|
+
{ id: 'id_4', created_at: new Date(baseDate.getTime() + 4000).toISOString(), value: 4 },
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
// Create pre-sorted database sources
|
|
378
|
+
const orderBy = defaultTimeOrderBy('desc');
|
|
379
|
+
const source1 = createSortedDatabaseDataSource(items1, orderBy);
|
|
380
|
+
const source2 = createSortedDatabaseDataSource(items2, orderBy);
|
|
381
|
+
|
|
382
|
+
const result = await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, orderBy);
|
|
383
|
+
|
|
384
|
+
expect(result.data[0].value).toBe(4); // Latest item should be first
|
|
385
|
+
expect(result.data[3].value).toBe(1); // Oldest item should be last
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should use id as secondary sort key', async () => {
|
|
389
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
390
|
+
const items = [
|
|
391
|
+
{ id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 1 },
|
|
392
|
+
{ id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString(), value: 2 },
|
|
393
|
+
];
|
|
394
|
+
const source = createSortingTestDataSource(items);
|
|
395
|
+
|
|
396
|
+
// Sort by timestamp ascending, then by id ascending
|
|
397
|
+
const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('asc', 'id'));
|
|
398
|
+
|
|
399
|
+
// Verify secondary sort key: When timestamps are equal, sort by id in ascending order
|
|
400
|
+
expect(result.data[0].id).toBe('in_LA0hts9lHaDycDii3TEfCzkb'); // L comes before r
|
|
401
|
+
expect(result.data[1].id).toBe('in_rBgXh8srXYXAXPs1Sn4XsJy5');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should use value as secondary sort key', async () => {
|
|
405
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
406
|
+
const items = [
|
|
407
|
+
{ id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 2 },
|
|
408
|
+
{ id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString(), value: 1 },
|
|
409
|
+
];
|
|
410
|
+
const source = createSortingTestDataSource(items);
|
|
411
|
+
|
|
412
|
+
// Sort by timestamp ascending, then by value ascending
|
|
413
|
+
const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('asc', 'value'));
|
|
414
|
+
|
|
415
|
+
// Verify secondary sort key: When timestamps are equal, sort by value in ascending order
|
|
416
|
+
expect(result.data[0].value).toBe(1);
|
|
417
|
+
expect(result.data[1].value).toBe(2);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('should respect primary sort order for secondary field', async () => {
|
|
421
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
422
|
+
const items = [
|
|
423
|
+
{ id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 1 },
|
|
424
|
+
{ id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString(), value: 2 },
|
|
425
|
+
];
|
|
426
|
+
const source = createSortingTestDataSource(items);
|
|
427
|
+
|
|
428
|
+
// Sort by timestamp descending, then by value descending
|
|
429
|
+
const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('desc', 'value'));
|
|
430
|
+
|
|
431
|
+
// Verify secondary sort key: When timestamps are equal, sort by value in descending order
|
|
432
|
+
expect(result.data[0].value).toBe(2);
|
|
433
|
+
expect(result.data[1].value).toBe(1);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should handle items with same timestamp without secondary field', async () => {
|
|
437
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
438
|
+
const items = [
|
|
439
|
+
{ id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 2 },
|
|
440
|
+
{ id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString(), value: 1 },
|
|
441
|
+
];
|
|
442
|
+
const source = createSortingTestDataSource(items);
|
|
443
|
+
|
|
444
|
+
// Sort by timestamp only, without specifying secondary sort field (defaults to 'desc' order with 'id' secondary)
|
|
445
|
+
const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('desc'));
|
|
446
|
+
|
|
447
|
+
// Verify: When timestamps are equal, sort by id in descending order (because primary sort is 'desc')
|
|
448
|
+
expect(result.data[0].id).toBe('in_rBgXh8srXYXAXPs1Sn4XsJy5'); // r comes after L in descending order
|
|
449
|
+
expect(result.data[1].id).toBe('in_LA0hts9lHaDycDii3TEfCzkb');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should handle items with undefined secondary field values', async () => {
|
|
453
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
454
|
+
const items = [
|
|
455
|
+
{ id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 2 },
|
|
456
|
+
{ id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString() }, // value undefined
|
|
457
|
+
];
|
|
458
|
+
const source = createSortingTestDataSource(items);
|
|
459
|
+
|
|
460
|
+
// Specify value as secondary sort field, but one item has no value (should fall back to id)
|
|
461
|
+
const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('desc', 'value'));
|
|
462
|
+
|
|
463
|
+
// Verify: When secondary field value is undefined, use id as fallback sort in descending order
|
|
464
|
+
expect(result.data[0].id).toBe('in_rBgXh8srXYXAXPs1Sn4XsJy5'); // r comes after L in descending order
|
|
465
|
+
expect(result.data[1].id).toBe('in_LA0hts9lHaDycDii3TEfCzkb');
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('should use id as default secondary sort key', async () => {
|
|
469
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
470
|
+
const items = [
|
|
471
|
+
{ id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 2 },
|
|
472
|
+
{ id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString(), value: 1 },
|
|
473
|
+
];
|
|
474
|
+
const source = createSortingTestDataSource(items);
|
|
475
|
+
|
|
476
|
+
// No secondary sort field specified, should use id by default with descending order
|
|
477
|
+
const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('desc'));
|
|
478
|
+
|
|
479
|
+
// Verify: When timestamps are equal, sort by id in descending order
|
|
480
|
+
expect(result.data[0].id).toBe('in_rBgXh8srXYXAXPs1Sn4XsJy5'); // r comes after L in descending order
|
|
481
|
+
expect(result.data[1].id).toBe('in_LA0hts9lHaDycDii3TEfCzkb');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should use id as fallback when secondary field is undefined', async () => {
|
|
485
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
486
|
+
const items = [
|
|
487
|
+
{ id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 2 },
|
|
488
|
+
{ id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString() }, // value undefined
|
|
489
|
+
];
|
|
490
|
+
const source = createSortingTestDataSource(items);
|
|
491
|
+
|
|
492
|
+
// Specify value as secondary sort field, but one item has no value
|
|
493
|
+
const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('desc', 'value'));
|
|
494
|
+
|
|
495
|
+
// Verify: When secondary field value is undefined, use id as fallback sort
|
|
496
|
+
expect(result.data[0].id).toBe('in_rBgXh8srXYXAXPs1Sn4XsJy5');
|
|
497
|
+
expect(result.data[1].id).toBe('in_LA0hts9lHaDycDii3TEfCzkb');
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('should use id as fallback for unsupported field types', async () => {
|
|
501
|
+
const baseDate = new Date('2024-01-01T00:00:00Z');
|
|
502
|
+
const items = [
|
|
503
|
+
{ id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: { complex: 'object' } },
|
|
504
|
+
{ id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString(), value: { complex: 'object' } },
|
|
505
|
+
];
|
|
506
|
+
const source = createSortingTestDataSource(items);
|
|
507
|
+
|
|
508
|
+
// Specify value as secondary sort field, but value is complex object
|
|
509
|
+
const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('desc', 'value'));
|
|
510
|
+
|
|
511
|
+
// Verify: When secondary field type is unsupported, use id as fallback sort
|
|
512
|
+
expect(result.data[0].id).toBe('in_rBgXh8srXYXAXPs1Sn4XsJy5');
|
|
513
|
+
expect(result.data[1].id).toBe('in_LA0hts9lHaDycDii3TEfCzkb');
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
describe('Performance', () => {
|
|
518
|
+
it('should handle large data sources efficiently', async () => {
|
|
519
|
+
const largeItems = createTestItems(1000);
|
|
520
|
+
const source = createTestDataSource(largeItems);
|
|
521
|
+
|
|
522
|
+
const startTime = Date.now();
|
|
523
|
+
const result = await mergePaginate([source], { page: 10, pageSize: 10 }, defaultTimeOrderBy());
|
|
524
|
+
const duration = Date.now() - startTime;
|
|
525
|
+
|
|
526
|
+
expect(result.total).toBe(1000);
|
|
527
|
+
expect(result.data).toHaveLength(10);
|
|
528
|
+
expect(duration).toBeLessThan(100); // Ensure processing time is within reasonable range
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should not fetch all data when not necessary', async () => {
|
|
532
|
+
const source1 = createTestDataSource(createTestItems(100));
|
|
533
|
+
const source2 = createTestDataSource(createTestItems(100));
|
|
534
|
+
|
|
535
|
+
// Mock fetch method calls
|
|
536
|
+
const spy1 = jest.spyOn(source1, 'fetch');
|
|
537
|
+
const spy2 = jest.spyOn(source2, 'fetch');
|
|
538
|
+
|
|
539
|
+
await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, defaultTimeOrderBy());
|
|
540
|
+
|
|
541
|
+
// Ensure no data is fetched all at once
|
|
542
|
+
expect(spy1).toHaveBeenCalledWith(expect.any(Number), expect.any(Number));
|
|
543
|
+
expect(spy2).toHaveBeenCalledWith(expect.any(Number), expect.any(Number));
|
|
544
|
+
|
|
545
|
+
spy1.mockRestore();
|
|
546
|
+
spy2.mockRestore();
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
});
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.47",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -47,19 +47,19 @@
|
|
|
47
47
|
"@abtnode/cron": "^1.16.43",
|
|
48
48
|
"@arcblock/did": "^1.20.11",
|
|
49
49
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
50
|
-
"@arcblock/did-connect": "^2.13.
|
|
50
|
+
"@arcblock/did-connect": "^2.13.47",
|
|
51
51
|
"@arcblock/did-util": "^1.20.11",
|
|
52
52
|
"@arcblock/jwt": "^1.20.11",
|
|
53
|
-
"@arcblock/ux": "^2.13.
|
|
53
|
+
"@arcblock/ux": "^2.13.47",
|
|
54
54
|
"@arcblock/validator": "^1.20.11",
|
|
55
|
-
"@blocklet/did-space-js": "^1.0.
|
|
55
|
+
"@blocklet/did-space-js": "^1.0.56",
|
|
56
56
|
"@blocklet/js-sdk": "^1.16.43",
|
|
57
57
|
"@blocklet/logger": "^1.16.43",
|
|
58
|
-
"@blocklet/payment-react": "1.18.
|
|
58
|
+
"@blocklet/payment-react": "1.18.47",
|
|
59
59
|
"@blocklet/sdk": "^1.16.43",
|
|
60
|
-
"@blocklet/ui-react": "^2.13.
|
|
61
|
-
"@blocklet/uploader": "^0.1.
|
|
62
|
-
"@blocklet/xss": "^0.1.
|
|
60
|
+
"@blocklet/ui-react": "^2.13.47",
|
|
61
|
+
"@blocklet/uploader": "^0.1.93",
|
|
62
|
+
"@blocklet/xss": "^0.1.36",
|
|
63
63
|
"@mui/icons-material": "^5.16.6",
|
|
64
64
|
"@mui/lab": "^5.0.0-alpha.173",
|
|
65
65
|
"@mui/material": "^5.16.6",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"devDependencies": {
|
|
124
124
|
"@abtnode/types": "^1.16.43",
|
|
125
125
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
126
|
-
"@blocklet/payment-types": "1.18.
|
|
126
|
+
"@blocklet/payment-types": "1.18.47",
|
|
127
127
|
"@types/cookie-parser": "^1.4.7",
|
|
128
128
|
"@types/cors": "^2.8.17",
|
|
129
129
|
"@types/debug": "^4.1.12",
|
|
@@ -169,5 +169,5 @@
|
|
|
169
169
|
"parser": "typescript"
|
|
170
170
|
}
|
|
171
171
|
},
|
|
172
|
-
"gitHead": "
|
|
172
|
+
"gitHead": "e781286c5a20e54ef0eb7e029505f32c00cb33e2"
|
|
173
173
|
}
|
|
@@ -21,7 +21,7 @@ import SubscriptionDescription from '../description';
|
|
|
21
21
|
import SubscriptionActions from './actions';
|
|
22
22
|
import SubscriptionStatus from '../status';
|
|
23
23
|
import useDelayedLoading from '../../../hooks/loading';
|
|
24
|
-
import { isWillCanceled } from '../../../libs/util';
|
|
24
|
+
import { formatProxyUrl, isWillCanceled } from '../../../libs/util';
|
|
25
25
|
|
|
26
26
|
type SubscriptionListResponse = {
|
|
27
27
|
count: number;
|
|
@@ -161,31 +161,41 @@ export default function CurrentSubscriptions({
|
|
|
161
161
|
justifyContent="space-between"
|
|
162
162
|
onClick={() => onClickSubscription(subscription)}>
|
|
163
163
|
<Stack direction="row" spacing={1.5} alignItems="center">
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
164
|
+
{subscription.metadata?.appLogo ? (
|
|
165
|
+
<Avatar
|
|
166
|
+
src={formatProxyUrl(subscription.metadata?.appLogo)}
|
|
167
|
+
alt={subscription.metadata?.appName}
|
|
168
|
+
variant="rounded"
|
|
169
|
+
sx={size}
|
|
170
|
+
/>
|
|
171
|
+
) : (
|
|
172
|
+
<AvatarGroup
|
|
173
|
+
max={2}
|
|
174
|
+
sx={{
|
|
175
|
+
'& .MuiAvatar-colorDefault': {
|
|
176
|
+
...size,
|
|
177
|
+
boxSizing: 'border-box',
|
|
178
|
+
},
|
|
179
|
+
}}>
|
|
180
|
+
{subscription.items.slice(0, 2).map((item) =>
|
|
181
|
+
item.price.product.images.length > 0 ? (
|
|
182
|
+
// @ts-ignore
|
|
183
|
+
<Avatar
|
|
184
|
+
key={item.price.product_id}
|
|
185
|
+
src={item.price.product.images[0]}
|
|
186
|
+
alt={item.price.product.name}
|
|
187
|
+
variant="rounded"
|
|
188
|
+
sx={size}
|
|
189
|
+
/>
|
|
190
|
+
) : (
|
|
191
|
+
<Avatar key={item.price.product_id} variant="rounded" sx={size}>
|
|
192
|
+
{item.price.product.name.slice(0, 1)}
|
|
193
|
+
</Avatar>
|
|
194
|
+
)
|
|
195
|
+
)}
|
|
196
|
+
</AvatarGroup>
|
|
197
|
+
)}
|
|
198
|
+
|
|
189
199
|
<Stack direction="column" spacing={0.25}>
|
|
190
200
|
<SubscriptionDescription
|
|
191
201
|
subscription={subscription}
|