payment-kit 1.25.7 → 1.25.10

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.
Files changed (36) hide show
  1. package/api/src/crons/index.ts +24 -0
  2. package/api/src/libs/archive/config.ts +254 -0
  3. package/api/src/libs/archive/executor.ts +729 -0
  4. package/api/src/libs/archive/index.ts +7 -0
  5. package/api/src/libs/archive/lock.ts +50 -0
  6. package/api/src/libs/archive/policy.ts +55 -0
  7. package/api/src/libs/archive/query.ts +136 -0
  8. package/api/src/libs/archive/snapshot.ts +291 -0
  9. package/api/src/libs/archive/store.ts +200 -0
  10. package/api/src/libs/credit-grant.ts +133 -0
  11. package/api/src/queues/archive.ts +32 -0
  12. package/api/src/routes/archive.ts +176 -0
  13. package/api/src/routes/credit-grants.ts +57 -4
  14. package/api/src/routes/index.ts +2 -0
  15. package/api/src/routes/payment-stats.ts +167 -20
  16. package/api/src/store/migrations/20260129-add-grantor-did-index.ts +52 -0
  17. package/api/src/store/migrations/20260203-archive.ts +12 -0
  18. package/api/src/store/migrations/20260204-revenue-snapshot.ts +19 -0
  19. package/api/src/store/models/archive-lock.ts +55 -0
  20. package/api/src/store/models/archive-metadata.ts +132 -0
  21. package/api/src/store/models/index.ts +9 -0
  22. package/api/src/store/models/revenue-snapshot.ts +110 -0
  23. package/api/tests/libs/archive-config.spec.ts +185 -0
  24. package/api/tests/libs/archive-executor.spec.ts +678 -0
  25. package/api/tests/libs/archive-lock.spec.ts +130 -0
  26. package/api/tests/libs/archive-policy.spec.ts +255 -0
  27. package/api/tests/libs/archive-query.spec.ts +267 -0
  28. package/api/tests/libs/archive-store.spec.ts +159 -0
  29. package/api/tests/libs/credit-grant.spec.ts +184 -0
  30. package/blocklet.prefs.json +187 -0
  31. package/blocklet.yml +1 -1
  32. package/package.json +10 -10
  33. package/src/locales/en.tsx +4 -0
  34. package/src/locales/zh.tsx +4 -0
  35. package/src/pages/admin/overview.tsx +2 -0
  36. package/vite.config.ts +1 -0
@@ -0,0 +1,159 @@
1
+ import fs from 'fs';
2
+
3
+ import {
4
+ getArchiveDir,
5
+ getArchiveFilePath,
6
+ listArchiveFiles,
7
+ getFileSize,
8
+ cleanupOldArchiveFiles,
9
+ } from '../../src/libs/archive/store';
10
+
11
+ jest.mock('@blocklet/sdk/lib/config', () => ({
12
+ __esModule: true,
13
+ default: {
14
+ env: {
15
+ dataDir: '/tmp/test-payment-kit',
16
+ },
17
+ },
18
+ }));
19
+
20
+ jest.mock('fs');
21
+ jest.mock('../../src/libs/logger', () => ({
22
+ __esModule: true,
23
+ default: {
24
+ info: jest.fn(),
25
+ warn: jest.fn(),
26
+ error: jest.fn(),
27
+ },
28
+ }));
29
+
30
+ describe('archive/store', () => {
31
+ beforeEach(() => {
32
+ jest.clearAllMocks();
33
+ });
34
+
35
+ describe('getArchiveDir', () => {
36
+ it('should create archive directory if it does not exist', () => {
37
+ (fs.existsSync as jest.Mock).mockReturnValue(false);
38
+ (fs.mkdirSync as jest.Mock).mockReturnValue(undefined);
39
+
40
+ const result = getArchiveDir();
41
+
42
+ expect(result).toContain('archive');
43
+ expect(fs.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('archive'), { recursive: true });
44
+ });
45
+
46
+ it('should not create directory if it already exists', () => {
47
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
48
+
49
+ getArchiveDir();
50
+
51
+ expect(fs.mkdirSync).not.toHaveBeenCalled();
52
+ });
53
+ });
54
+
55
+ describe('getArchiveFilePath', () => {
56
+ it('should return full path for archive file', () => {
57
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
58
+
59
+ const result = getArchiveFilePath('archive-2024.db');
60
+
61
+ expect(result).toContain('archive-2024.db');
62
+ expect(result).toContain('archive');
63
+ });
64
+ });
65
+
66
+ describe('listArchiveFiles', () => {
67
+ it('should return sorted list of .db files', () => {
68
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
69
+ (fs.readdirSync as jest.Mock).mockReturnValue([
70
+ 'archive-2025.db',
71
+ 'archive-2023.db',
72
+ 'archive-2024.db',
73
+ 'some-other-file.txt',
74
+ ]);
75
+
76
+ const result = listArchiveFiles();
77
+
78
+ expect(result).toHaveLength(3);
79
+ expect(result[0]).toContain('archive-2023.db');
80
+ expect(result[1]).toContain('archive-2024.db');
81
+ expect(result[2]).toContain('archive-2025.db');
82
+ });
83
+
84
+ it('should return empty array when no .db files', () => {
85
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
86
+ (fs.readdirSync as jest.Mock).mockReturnValue(['readme.txt', 'config.json']);
87
+
88
+ const result = listArchiveFiles();
89
+
90
+ expect(result).toHaveLength(0);
91
+ });
92
+ });
93
+
94
+ describe('getFileSize', () => {
95
+ it('should return file size in bytes', () => {
96
+ (fs.statSync as jest.Mock).mockReturnValue({ size: 1024 });
97
+
98
+ const result = getFileSize('/tmp/archive.db');
99
+
100
+ expect(result).toBe(1024);
101
+ });
102
+
103
+ it('should return 0 when file does not exist', () => {
104
+ (fs.statSync as jest.Mock).mockImplementation(() => {
105
+ throw new Error('ENOENT');
106
+ });
107
+
108
+ const result = getFileSize('/tmp/nonexistent.db');
109
+
110
+ expect(result).toBe(0);
111
+ });
112
+ });
113
+
114
+ describe('cleanupOldArchiveFiles', () => {
115
+ it('should not remove files when under max limit', () => {
116
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
117
+ (fs.readdirSync as jest.Mock).mockReturnValue(['archive-2023.db', 'archive-2024.db']);
118
+
119
+ const result = cleanupOldArchiveFiles(10);
120
+
121
+ expect(result).toHaveLength(0);
122
+ expect(fs.unlinkSync).not.toHaveBeenCalled();
123
+ });
124
+
125
+ it('should remove oldest files when exceeding max limit', () => {
126
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
127
+ (fs.readdirSync as jest.Mock).mockReturnValue([
128
+ 'archive-2021.db',
129
+ 'archive-2022.db',
130
+ 'archive-2023.db',
131
+ 'archive-2024.db',
132
+ ]);
133
+ (fs.unlinkSync as jest.Mock).mockReturnValue(undefined);
134
+
135
+ const result = cleanupOldArchiveFiles(2);
136
+
137
+ expect(result).toHaveLength(2);
138
+ expect(result).toContain('archive-2021.db');
139
+ expect(result).toContain('archive-2022.db');
140
+ expect(fs.unlinkSync).toHaveBeenCalledTimes(2);
141
+ });
142
+
143
+ it('should continue cleanup even if one file fails to delete', () => {
144
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
145
+ (fs.readdirSync as jest.Mock).mockReturnValue(['archive-2022.db', 'archive-2023.db', 'archive-2024.db']);
146
+ (fs.unlinkSync as jest.Mock)
147
+ .mockImplementationOnce(() => {
148
+ throw new Error('Permission denied');
149
+ })
150
+ .mockReturnValue(undefined);
151
+
152
+ const result = cleanupOldArchiveFiles(1);
153
+
154
+ expect(fs.unlinkSync).toHaveBeenCalledTimes(2);
155
+ expect(result).toHaveLength(1);
156
+ expect(result).toContain('archive-2023.db');
157
+ });
158
+ });
159
+ });
@@ -0,0 +1,184 @@
1
+ import { Op } from 'sequelize';
2
+
3
+ import { getCreditGrantStats } from '../../src/libs/credit-grant';
4
+ import { CreditGrant, PaymentCurrency } from '../../src/store/models';
5
+
6
+ jest.mock('../../src/libs/logger', () => ({
7
+ __esModule: true,
8
+ default: {
9
+ info: jest.fn(),
10
+ warn: jest.fn(),
11
+ error: jest.fn(),
12
+ debug: jest.fn(),
13
+ },
14
+ }));
15
+
16
+ jest.mock('../../src/libs/subscription', () => ({
17
+ getMeterPriceIdsFromSubscription: jest.fn(),
18
+ }));
19
+
20
+ jest.mock('../../src/store/models', () => ({
21
+ CreditGrant: {
22
+ findAll: jest.fn(),
23
+ },
24
+ PaymentCurrency: {
25
+ findByPk: jest.fn(),
26
+ },
27
+ Customer: {
28
+ findByPk: jest.fn(),
29
+ },
30
+ Subscription: {
31
+ findByPk: jest.fn(),
32
+ },
33
+ }));
34
+
35
+ describe('libs/credit-grant.ts', () => {
36
+ beforeEach(() => {
37
+ jest.clearAllMocks();
38
+ jest.restoreAllMocks();
39
+ });
40
+
41
+ it('aggregates daily stats and totals', async () => {
42
+ const currencyJson = { id: 'cur_1', name: 'USD', symbol: '$', decimal: 2 };
43
+ (PaymentCurrency.findByPk as jest.Mock).mockResolvedValue({
44
+ ...currencyJson,
45
+ toJSON: () => currencyJson,
46
+ });
47
+
48
+ (CreditGrant.findAll as jest.Mock).mockResolvedValue([
49
+ {
50
+ amount: '100',
51
+ remaining_amount: '40',
52
+ created_at: new Date('2024-01-01T01:00:00Z'),
53
+ },
54
+ {
55
+ amount: '200',
56
+ remaining_amount: '200',
57
+ created_at: new Date('2024-01-01T10:00:00Z'),
58
+ },
59
+ {
60
+ amount: '50',
61
+ remaining_amount: '10',
62
+ created_at: new Date('2024-01-02T05:00:00Z'),
63
+ },
64
+ ]);
65
+
66
+ const result = await getCreditGrantStats({
67
+ currencyId: 'cur_1',
68
+ startDate: 1704067200,
69
+ endDate: 1704240000,
70
+ });
71
+
72
+ expect(PaymentCurrency.findByPk).toHaveBeenCalledWith('cur_1', {
73
+ attributes: ['id', 'name', 'symbol', 'decimal'],
74
+ });
75
+
76
+ const callArg = (CreditGrant.findAll as jest.Mock).mock.calls[0][0];
77
+ expect(callArg.attributes).toEqual(['amount', 'remaining_amount', 'created_at']);
78
+ expect(callArg.raw).toBe(true);
79
+
80
+ expect(result.stats).toEqual({
81
+ currency_id: 'cur_1',
82
+ currency: currencyJson,
83
+ grant_count: 3,
84
+ total_granted: '350',
85
+ total_remaining: '250',
86
+ total_consumed: '100',
87
+ });
88
+
89
+ expect(result.daily_stats).toEqual([
90
+ {
91
+ date: '2024-01-01',
92
+ currency_id: 'cur_1',
93
+ grant_count: 2,
94
+ total_granted: '300',
95
+ total_remaining: '240',
96
+ total_consumed: '60',
97
+ },
98
+ {
99
+ date: '2024-01-02',
100
+ currency_id: 'cur_1',
101
+ grant_count: 1,
102
+ total_granted: '50',
103
+ total_remaining: '10',
104
+ total_consumed: '40',
105
+ },
106
+ ]);
107
+ });
108
+
109
+ it('groups grants by day using timezone offset', async () => {
110
+ const currencyJson = { id: 'cur_2', name: 'USD', symbol: '$', decimal: 2 };
111
+ (PaymentCurrency.findByPk as jest.Mock).mockResolvedValue({
112
+ ...currencyJson,
113
+ toJSON: () => currencyJson,
114
+ });
115
+
116
+ (CreditGrant.findAll as jest.Mock).mockResolvedValue([
117
+ {
118
+ amount: '10',
119
+ remaining_amount: '5',
120
+ created_at: new Date('2024-01-01T23:30:00Z'),
121
+ },
122
+ {
123
+ amount: '20',
124
+ remaining_amount: '0',
125
+ created_at: new Date('2024-01-02T01:00:00Z'),
126
+ },
127
+ ]);
128
+
129
+ const result = await getCreditGrantStats({
130
+ currencyId: 'cur_2',
131
+ startDate: 1704067200,
132
+ endDate: 1704240000,
133
+ timezoneOffset: 480,
134
+ });
135
+
136
+ expect(result.daily_stats).toEqual([
137
+ {
138
+ date: '2024-01-02',
139
+ currency_id: 'cur_2',
140
+ grant_count: 2,
141
+ total_granted: '30',
142
+ total_remaining: '5',
143
+ total_consumed: '25',
144
+ },
145
+ ]);
146
+ });
147
+
148
+ it('builds where clause with grantedBy and category filters', async () => {
149
+ const currencyJson = { id: 'cur_3', name: 'USD', symbol: '$', decimal: 2 };
150
+ (PaymentCurrency.findByPk as jest.Mock).mockResolvedValue({
151
+ ...currencyJson,
152
+ toJSON: () => currencyJson,
153
+ });
154
+ (CreditGrant.findAll as jest.Mock).mockResolvedValue([]);
155
+
156
+ const startDate = 1704067200;
157
+ const endDate = 1704153600;
158
+
159
+ const result = await getCreditGrantStats({
160
+ currencyId: 'cur_3',
161
+ startDate,
162
+ endDate,
163
+ grantedBy: 'did:example:123',
164
+ category: 'paid',
165
+ });
166
+
167
+ const callArg = (CreditGrant.findAll as jest.Mock).mock.calls[0][0];
168
+ expect(callArg.where.currency_id).toBe('cur_3');
169
+ expect(callArg.where['metadata.granted_by']).toBe('did:example:123');
170
+ expect(callArg.where.category).toBe('paid');
171
+ expect(callArg.where.created_at[Op.gte]).toEqual(new Date(startDate * 1000));
172
+ expect(callArg.where.created_at[Op.lte]).toEqual(new Date(endDate * 1000));
173
+
174
+ expect(result.stats).toEqual({
175
+ currency_id: 'cur_3',
176
+ currency: currencyJson,
177
+ grant_count: 0,
178
+ total_granted: '0',
179
+ total_remaining: '0',
180
+ total_consumed: '0',
181
+ });
182
+ expect(result.daily_stats).toEqual([]);
183
+ });
184
+ });
@@ -0,0 +1,187 @@
1
+ {
2
+ "form": {
3
+ "labelCol": 6,
4
+ "wrapperCol": 12
5
+ },
6
+ "schema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "8ezxt923qyy": {
10
+ "type": "void",
11
+ "x-component": "Card",
12
+ "x-component-props": {
13
+ "title": "Config"
14
+ },
15
+ "x-designable-id": "8ezxt923qyy",
16
+ "properties": {
17
+ "wvny47lybcc": {
18
+ "type": "void",
19
+ "x-component": "FormCollapse",
20
+ "x-component-props": {},
21
+ "name": "Data Retention",
22
+ "x-designable-id": "wvny47lybcc",
23
+ "properties": {
24
+ "5tiydb79qp2": {
25
+ "type": "void",
26
+ "x-component": "FormCollapse.CollapsePanel",
27
+ "x-component-props": {
28
+ "header": "Data Retention"
29
+ },
30
+ "x-designable-id": "5tiydb79qp2",
31
+ "properties": {
32
+ "retentionEnabled": {
33
+ "type": "boolean",
34
+ "title": "Enable Data Retention ",
35
+ "x-decorator": "FormItem",
36
+ "x-component": "Switch",
37
+ "x-validator": [],
38
+ "x-component-props": {},
39
+ "x-decorator-props": {},
40
+ "name": "retentionEnabled",
41
+ "default": false,
42
+ "description": "Archive expired data to reduce database size",
43
+ "x-designable-id": "886b4pe1o1y",
44
+ "x-index": 0
45
+ },
46
+ "scheduleEnabled": {
47
+ "type": "boolean",
48
+ "title": "Enable Scheduled Archive",
49
+ "x-decorator": "FormItem",
50
+ "x-component": "Switch",
51
+ "x-validator": [],
52
+ "x-component-props": {},
53
+ "x-decorator-props": {},
54
+ "description": "Run archive job automatically every day",
55
+ "name": "scheduleEnabled",
56
+ "default": true,
57
+ "x-reactions": {
58
+ "dependencies": [
59
+ {
60
+ "property": "value",
61
+ "type": "boolean",
62
+ "source": "8ezxt923qyy.wvny47lybcc.5tiydb79qp2.retentionEnabled",
63
+ "name": "retentionEnabled"
64
+ }
65
+ ],
66
+ "fulfill": {
67
+ "state": {
68
+ "visible": "{{$deps.retentionEnabled}}"
69
+ }
70
+ }
71
+ },
72
+ "x-designable-id": "53cmu3qg9j3",
73
+ "x-index": 1
74
+ },
75
+ "scheduleHour": {
76
+ "type": "number",
77
+ "title": " Archive Hour (0-23) ",
78
+ "x-decorator": "FormItem",
79
+ "x-component": "NumberPicker",
80
+ "x-validator": "number",
81
+ "x-component-props": {
82
+ "min": 0,
83
+ "max": 23
84
+ },
85
+ "x-decorator-props": {},
86
+ "name": "scheduleHour",
87
+ "default": 2,
88
+ "description": "Hour of day to\nrun the archive job (server Local\ntime)",
89
+ "x-reactions": {
90
+ "dependencies": [
91
+ {
92
+ "property": "value",
93
+ "type": "boolean",
94
+ "source": "8ezxt923qyy.wvny47lybcc.5tiydb79qp2.retentionEnabled",
95
+ "name": "retentionEnabled"
96
+ },
97
+ {
98
+ "property": "value",
99
+ "type": "boolean",
100
+ "source": "8ezxt923qyy.wvny47lybcc.5tiydb79qp2.scheduleEnabled",
101
+ "name": "scheduleEnabled"
102
+ }
103
+ ],
104
+ "fulfill": {
105
+ "state": {
106
+ "visible": "{{$deps.retentionEnabled === true && $deps.scheduleEnabled === true}}"
107
+ }
108
+ }
109
+ },
110
+ "x-designable-id": "dvdqp2aaszw",
111
+ "x-index": 2
112
+ },
113
+ "batchSize": {
114
+ "type": "number",
115
+ "title": "Batch Size ",
116
+ "x-decorator": "FormItem",
117
+ "x-component": "NumberPicker",
118
+ "x-validator": [],
119
+ "x-component-props": {
120
+ "min": 100
121
+ },
122
+ "x-decorator-props": {},
123
+ "name": "batchSize",
124
+ "default": 500,
125
+ "description": "Number of records to archive per batch. Smaller\nvalues reduce database load",
126
+ "x-reactions": {
127
+ "dependencies": [
128
+ {
129
+ "property": "value",
130
+ "type": "boolean",
131
+ "source": "8ezxt923qyy.wvny47lybcc.5tiydb79qp2.retentionEnabled",
132
+ "name": "retentionEnabled"
133
+ }
134
+ ],
135
+ "fulfill": {
136
+ "state": {
137
+ "visible": "{{$deps.retentionEnabled}}"
138
+ }
139
+ }
140
+ },
141
+ "x-designable-id": "5mdfl84f6xw",
142
+ "x-index": 3
143
+ },
144
+ "minFreeDiskMB": {
145
+ "type": "number",
146
+ "title": "Min Free Disk Space (MB)",
147
+ "x-decorator": "FormItem",
148
+ "x-component": "NumberPicker",
149
+ "x-validator": [],
150
+ "x-component-props": {
151
+ "min": 100
152
+ },
153
+ "x-decorator-props": {},
154
+ "name": "minFreeDiskMB",
155
+ "default": 1000,
156
+ "description": "Stop archiving when free disk space falls below this threshold (default 1GB)",
157
+ "x-reactions": {
158
+ "dependencies": [
159
+ {
160
+ "property": "value",
161
+ "type": "boolean",
162
+ "source": "8ezxt923qyy.wvny47lybcc.5tiydb79qp2.retentionEnabled",
163
+ "name": "retentionEnabled"
164
+ }
165
+ ],
166
+ "fulfill": {
167
+ "state": {
168
+ "visible": "{{$deps.retentionEnabled}}"
169
+ }
170
+ }
171
+ },
172
+ "x-designable-id": "ud3govolh1e",
173
+ "x-index": 4
174
+ }
175
+ },
176
+ "x-index": 0
177
+ }
178
+ },
179
+ "x-index": 0
180
+ }
181
+ },
182
+ "x-index": 0
183
+ }
184
+ },
185
+ "x-designable-id": "yfw4ta3wi0l"
186
+ }
187
+ }
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.25.7
17
+ version: 1.25.10
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.25.7",
3
+ "version": "1.25.10",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "prelint": "npm run types",
@@ -47,23 +47,23 @@
47
47
  "dependencies": {
48
48
  "@abtnode/cron": "^1.17.8-beta-20260104-120132-cb5b1914",
49
49
  "@arcblock/did": "^1.28.5",
50
- "@arcblock/did-connect-react": "^3.4.7",
50
+ "@arcblock/did-connect-react": "^3.5.1",
51
51
  "@arcblock/did-connect-storage-nedb": "^1.8.0",
52
52
  "@arcblock/did-util": "^1.28.5",
53
53
  "@arcblock/jwt": "^1.28.5",
54
- "@arcblock/react-hooks": "^3.4.7",
55
- "@arcblock/ux": "^3.4.7",
54
+ "@arcblock/react-hooks": "^3.5.1",
55
+ "@arcblock/ux": "^3.5.1",
56
56
  "@arcblock/validator": "^1.28.5",
57
57
  "@arcblock/vc": "^1.28.5",
58
58
  "@blocklet/did-space-js": "^1.2.15",
59
59
  "@blocklet/error": "^0.3.5",
60
60
  "@blocklet/js-sdk": "^1.17.8-beta-20260104-120132-cb5b1914",
61
61
  "@blocklet/logger": "^1.17.8-beta-20260104-120132-cb5b1914",
62
- "@blocklet/payment-broker-client": "1.25.7",
63
- "@blocklet/payment-react": "1.25.7",
64
- "@blocklet/payment-vendor": "1.25.7",
62
+ "@blocklet/payment-broker-client": "1.25.10",
63
+ "@blocklet/payment-react": "1.25.10",
64
+ "@blocklet/payment-vendor": "1.25.10",
65
65
  "@blocklet/sdk": "^1.17.8-beta-20260104-120132-cb5b1914",
66
- "@blocklet/ui-react": "^3.4.7",
66
+ "@blocklet/ui-react": "^3.5.1",
67
67
  "@blocklet/uploader": "^0.3.19",
68
68
  "@blocklet/xss": "^0.3.16",
69
69
  "@mui/icons-material": "^7.1.2",
@@ -132,7 +132,7 @@
132
132
  "devDependencies": {
133
133
  "@abtnode/types": "^1.17.8-beta-20260104-120132-cb5b1914",
134
134
  "@arcblock/eslint-config-ts": "^0.3.3",
135
- "@blocklet/payment-types": "1.25.7",
135
+ "@blocklet/payment-types": "1.25.10",
136
136
  "@types/cookie-parser": "^1.4.9",
137
137
  "@types/cors": "^2.8.19",
138
138
  "@types/debug": "^4.1.12",
@@ -179,5 +179,5 @@
179
179
  "parser": "typescript"
180
180
  }
181
181
  },
182
- "gitHead": "0aa950a5d3f01a150ec21c66e2efeaf81ee1cc69"
182
+ "gitHead": "36417122ec9a0cfd1e289b4ca21d33f1b5d87a9a"
183
183
  }
@@ -164,6 +164,10 @@ export default flat({
164
164
  title: 'Total Income',
165
165
  subtitle: "User's actual payment",
166
166
  },
167
+ refundAmount: {
168
+ title: 'Refund Amount',
169
+ subtitle: 'Total refunds issued',
170
+ },
167
171
  costOfGoods: {
168
172
  title: 'Cost of Goods',
169
173
  subtitle: 'Supplier share',
@@ -162,6 +162,10 @@ export default flat({
162
162
  title: '总收入',
163
163
  subtitle: '用户实际支付',
164
164
  },
165
+ refundAmount: {
166
+ title: '退款金额',
167
+ subtitle: '已退款总额',
168
+ },
165
169
  costOfGoods: {
166
170
  title: '进货成本',
167
171
  subtitle: '供应商分成',
@@ -47,7 +47,9 @@ import DateRangePicker from '../../components/date-range-picker';
47
47
 
48
48
  type TRevenueStat = {
49
49
  totalRevenue: string;
50
+ refundAmount: string;
50
51
  promotionCost: string;
52
+ creditGrantCost: string;
51
53
  vendorCost: string;
52
54
  taxedRevenue: string;
53
55
  netRevenue: string;
package/vite.config.ts CHANGED
@@ -54,6 +54,7 @@ export default defineConfig(({ mode }) => {
54
54
  react({ babel: { plugins: isProduction ? [['lodash']] : [] } }),
55
55
  createBlockletPlugin({
56
56
  disableDynamicAssetHost: false,
57
+ chunkSizeLimit: 3500,
57
58
  }),
58
59
  svgr(),
59
60
  process.env.ANALYZE && visualizer({ open: true, gzipSize: true, brotliSize: true }),