galaxy-opc-plugin 0.1.1 → 0.2.0
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/index.ts +11 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/api/companies.ts +12 -1
- package/src/api/dashboard.ts +13 -2
- package/src/api/middleware.ts +44 -0
- package/src/api/rate-limiter.ts +79 -0
- package/src/api/routes.ts +19 -2
- package/src/db/sqlite-adapter.test.ts +304 -0
- package/src/db/sqlite-adapter.ts +1 -1
- package/src/opc/company-manager.test.ts +232 -0
- package/src/tools/acquisition-tool.test.ts +139 -0
- package/src/tools/acquisition-tool.ts +3 -3
- package/src/tools/asset-package-tool.ts +4 -4
- package/src/tools/finance-tool.test.ts +106 -0
- package/src/tools/finance-tool.ts +6 -4
- package/src/tools/hr-tool.test.ts +153 -0
- package/src/tools/hr-tool.ts +6 -4
- package/src/tools/investment-tool.ts +4 -4
- package/src/tools/legal-tool.ts +12 -7
- package/src/tools/lifecycle-tool.ts +5 -5
- package/src/tools/media-tool.ts +7 -5
- package/src/tools/monitoring-tool.ts +6 -4
- package/src/tools/opb-tool.ts +7 -7
- package/src/tools/opc-tool.ts +33 -23
- package/src/tools/procurement-tool.ts +4 -4
- package/src/tools/project-tool.ts +10 -6
- package/src/tools/schemas.ts +47 -43
- package/src/tools/staff-tool.ts +8 -6
- package/src/utils/tool-helper.ts +28 -2
- package/src/web/config-ui.ts +69 -15
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — CompanyManager 单元测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
6
|
+
import { SqliteAdapter } from "../db/sqlite-adapter.js";
|
|
7
|
+
import { CompanyManager } from "./company-manager.js";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
|
|
12
|
+
describe("CompanyManager", () => {
|
|
13
|
+
let db: SqliteAdapter;
|
|
14
|
+
let manager: CompanyManager;
|
|
15
|
+
let dbPath: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
dbPath = path.join(os.tmpdir(), `opc-test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
|
|
19
|
+
db = new SqliteAdapter(dbPath);
|
|
20
|
+
manager = new CompanyManager(db);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
db.close();
|
|
25
|
+
try { fs.unlinkSync(dbPath); } catch { /* ignore */ }
|
|
26
|
+
try { fs.unlinkSync(dbPath + "-wal"); } catch { /* ignore */ }
|
|
27
|
+
try { fs.unlinkSync(dbPath + "-shm"); } catch { /* ignore */ }
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("registerCompany", () => {
|
|
31
|
+
it("should create a company with pending status", () => {
|
|
32
|
+
const company = manager.registerCompany({
|
|
33
|
+
name: "测试科技",
|
|
34
|
+
industry: "互联网",
|
|
35
|
+
owner_name: "张三",
|
|
36
|
+
});
|
|
37
|
+
expect(company.id).toBeDefined();
|
|
38
|
+
expect(company.name).toBe("测试科技");
|
|
39
|
+
expect(company.industry).toBe("互联网");
|
|
40
|
+
expect(company.owner_name).toBe("张三");
|
|
41
|
+
expect(company.status).toBe("pending");
|
|
42
|
+
expect(company.registered_capital).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should accept optional fields", () => {
|
|
46
|
+
const company = manager.registerCompany({
|
|
47
|
+
name: "优质公司",
|
|
48
|
+
industry: "教育",
|
|
49
|
+
owner_name: "李四",
|
|
50
|
+
owner_contact: "13800138000",
|
|
51
|
+
registered_capital: 500000,
|
|
52
|
+
description: "在线教育平台",
|
|
53
|
+
});
|
|
54
|
+
expect(company.owner_contact).toBe("13800138000");
|
|
55
|
+
expect(company.registered_capital).toBe(500000);
|
|
56
|
+
expect(company.description).toBe("在线教育平台");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("getCompany", () => {
|
|
61
|
+
it("should return a company by id", () => {
|
|
62
|
+
const created = manager.registerCompany({
|
|
63
|
+
name: "查询测试",
|
|
64
|
+
industry: "零售",
|
|
65
|
+
owner_name: "王五",
|
|
66
|
+
});
|
|
67
|
+
const found = manager.getCompany(created.id);
|
|
68
|
+
expect(found).not.toBeNull();
|
|
69
|
+
expect(found!.name).toBe("查询测试");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should return null for non-existent id", () => {
|
|
73
|
+
expect(manager.getCompany("non-existent")).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("listCompanies", () => {
|
|
78
|
+
it("should list all companies", () => {
|
|
79
|
+
manager.registerCompany({ name: "A公司", industry: "IT", owner_name: "A" });
|
|
80
|
+
manager.registerCompany({ name: "B公司", industry: "金融", owner_name: "B" });
|
|
81
|
+
const list = manager.listCompanies();
|
|
82
|
+
expect(list.length).toBe(2);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should filter by status", () => {
|
|
86
|
+
const c = manager.registerCompany({ name: "C公司", industry: "IT", owner_name: "C" });
|
|
87
|
+
manager.registerCompany({ name: "D公司", industry: "IT", owner_name: "D" });
|
|
88
|
+
manager.activateCompany(c.id);
|
|
89
|
+
expect(manager.listCompanies("active").length).toBe(1);
|
|
90
|
+
expect(manager.listCompanies("pending").length).toBe(1);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("activateCompany", () => {
|
|
95
|
+
it("should transition pending → active", () => {
|
|
96
|
+
const c = manager.registerCompany({ name: "激活测试", industry: "IT", owner_name: "X" });
|
|
97
|
+
expect(c.status).toBe("pending");
|
|
98
|
+
const activated = manager.activateCompany(c.id);
|
|
99
|
+
expect(activated).not.toBeNull();
|
|
100
|
+
expect(activated!.status).toBe("active");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should return null for non-existent company", () => {
|
|
104
|
+
expect(manager.activateCompany("fake-id")).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("transitionStatus — valid transitions", () => {
|
|
109
|
+
it("pending → active", () => {
|
|
110
|
+
const c = manager.registerCompany({ name: "T1", industry: "IT", owner_name: "X" });
|
|
111
|
+
const result = manager.transitionStatus(c.id, "active");
|
|
112
|
+
expect(result!.status).toBe("active");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("pending → terminated", () => {
|
|
116
|
+
const c = manager.registerCompany({ name: "T2", industry: "IT", owner_name: "X" });
|
|
117
|
+
const result = manager.transitionStatus(c.id, "terminated");
|
|
118
|
+
expect(result!.status).toBe("terminated");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("active → suspended", () => {
|
|
122
|
+
const c = manager.registerCompany({ name: "T3", industry: "IT", owner_name: "X" });
|
|
123
|
+
manager.transitionStatus(c.id, "active");
|
|
124
|
+
const result = manager.transitionStatus(c.id, "suspended");
|
|
125
|
+
expect(result!.status).toBe("suspended");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("active → acquired", () => {
|
|
129
|
+
const c = manager.registerCompany({ name: "T4", industry: "IT", owner_name: "X" });
|
|
130
|
+
manager.transitionStatus(c.id, "active");
|
|
131
|
+
const result = manager.transitionStatus(c.id, "acquired");
|
|
132
|
+
expect(result!.status).toBe("acquired");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("active → packaged", () => {
|
|
136
|
+
const c = manager.registerCompany({ name: "T5", industry: "IT", owner_name: "X" });
|
|
137
|
+
manager.transitionStatus(c.id, "active");
|
|
138
|
+
const result = manager.transitionStatus(c.id, "packaged");
|
|
139
|
+
expect(result!.status).toBe("packaged");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("active → terminated", () => {
|
|
143
|
+
const c = manager.registerCompany({ name: "T6", industry: "IT", owner_name: "X" });
|
|
144
|
+
manager.transitionStatus(c.id, "active");
|
|
145
|
+
const result = manager.transitionStatus(c.id, "terminated");
|
|
146
|
+
expect(result!.status).toBe("terminated");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("suspended → active", () => {
|
|
150
|
+
const c = manager.registerCompany({ name: "T7", industry: "IT", owner_name: "X" });
|
|
151
|
+
manager.transitionStatus(c.id, "active");
|
|
152
|
+
manager.transitionStatus(c.id, "suspended");
|
|
153
|
+
const result = manager.transitionStatus(c.id, "active");
|
|
154
|
+
expect(result!.status).toBe("active");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("suspended → terminated", () => {
|
|
158
|
+
const c = manager.registerCompany({ name: "T8", industry: "IT", owner_name: "X" });
|
|
159
|
+
manager.transitionStatus(c.id, "active");
|
|
160
|
+
manager.transitionStatus(c.id, "suspended");
|
|
161
|
+
const result = manager.transitionStatus(c.id, "terminated");
|
|
162
|
+
expect(result!.status).toBe("terminated");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("acquired → terminated", () => {
|
|
166
|
+
const c = manager.registerCompany({ name: "T9", industry: "IT", owner_name: "X" });
|
|
167
|
+
manager.transitionStatus(c.id, "active");
|
|
168
|
+
manager.transitionStatus(c.id, "acquired");
|
|
169
|
+
const result = manager.transitionStatus(c.id, "terminated");
|
|
170
|
+
expect(result!.status).toBe("terminated");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("packaged → terminated", () => {
|
|
174
|
+
const c = manager.registerCompany({ name: "T10", industry: "IT", owner_name: "X" });
|
|
175
|
+
manager.transitionStatus(c.id, "active");
|
|
176
|
+
manager.transitionStatus(c.id, "packaged");
|
|
177
|
+
const result = manager.transitionStatus(c.id, "terminated");
|
|
178
|
+
expect(result!.status).toBe("terminated");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("transitionStatus — invalid transitions", () => {
|
|
183
|
+
it("pending → suspended should throw", () => {
|
|
184
|
+
const c = manager.registerCompany({ name: "E1", industry: "IT", owner_name: "X" });
|
|
185
|
+
expect(() => manager.transitionStatus(c.id, "suspended")).toThrow();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("pending → acquired should throw", () => {
|
|
189
|
+
const c = manager.registerCompany({ name: "E2", industry: "IT", owner_name: "X" });
|
|
190
|
+
expect(() => manager.transitionStatus(c.id, "acquired")).toThrow();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("terminated → active should throw", () => {
|
|
194
|
+
const c = manager.registerCompany({ name: "E3", industry: "IT", owner_name: "X" });
|
|
195
|
+
manager.transitionStatus(c.id, "terminated");
|
|
196
|
+
expect(() => manager.transitionStatus(c.id, "active")).toThrow(/不允许/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("acquired → active should throw", () => {
|
|
200
|
+
const c = manager.registerCompany({ name: "E4", industry: "IT", owner_name: "X" });
|
|
201
|
+
manager.transitionStatus(c.id, "active");
|
|
202
|
+
manager.transitionStatus(c.id, "acquired");
|
|
203
|
+
expect(() => manager.transitionStatus(c.id, "active")).toThrow();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("updateCompany", () => {
|
|
208
|
+
it("should update company fields", () => {
|
|
209
|
+
const c = manager.registerCompany({ name: "更新测试", industry: "IT", owner_name: "X" });
|
|
210
|
+
const updated = manager.updateCompany(c.id, { name: "新名称", industry: "金融" });
|
|
211
|
+
expect(updated).not.toBeNull();
|
|
212
|
+
expect(updated!.name).toBe("新名称");
|
|
213
|
+
expect(updated!.industry).toBe("金融");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should return null for non-existent company", () => {
|
|
217
|
+
expect(manager.updateCompany("fake-id", { name: "test" })).toBeNull();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("deleteCompany", () => {
|
|
222
|
+
it("should delete an existing company", () => {
|
|
223
|
+
const c = manager.registerCompany({ name: "删除测试", industry: "IT", owner_name: "X" });
|
|
224
|
+
expect(manager.deleteCompany(c.id)).toBe(true);
|
|
225
|
+
expect(manager.getCompany(c.id)).toBeNull();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should return false for non-existent company", () => {
|
|
229
|
+
expect(manager.deleteCompany("fake-id")).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — acquisition-tool 核心计算函数单元测试
|
|
3
|
+
*
|
|
4
|
+
* 测试收并购模块的亏损抵税计算逻辑。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
8
|
+
import { SqliteAdapter } from "../db/sqlite-adapter.js";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 亏损抵税计算: loss × 25% 企业所得税率
|
|
15
|
+
* 复制自 acquisition-tool.ts 的核心逻辑
|
|
16
|
+
*/
|
|
17
|
+
function calcTaxDeduction(lossAmount: number): number {
|
|
18
|
+
return lossAmount * 0.25;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("acquisition-tool calculations", () => {
|
|
22
|
+
describe("calcTaxDeduction — 亏损抵税计算", () => {
|
|
23
|
+
it("should calculate 25% tax deduction from loss", () => {
|
|
24
|
+
expect(calcTaxDeduction(100000)).toBe(25000);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should handle zero loss", () => {
|
|
28
|
+
expect(calcTaxDeduction(0)).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should handle large loss amounts", () => {
|
|
32
|
+
expect(calcTaxDeduction(10000000)).toBe(2500000); // 1000万亏损 → 250万抵税
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should handle decimal amounts", () => {
|
|
36
|
+
expect(calcTaxDeduction(33333.33)).toBeCloseTo(8333.33, 2);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("acquisition-tool database integration", () => {
|
|
42
|
+
let db: SqliteAdapter;
|
|
43
|
+
let dbPath: string;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
dbPath = path.join(os.tmpdir(), `opc-test-acq-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
|
|
47
|
+
db = new SqliteAdapter(dbPath);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
db.close();
|
|
52
|
+
try { fs.unlinkSync(dbPath); } catch { /* ignore */ }
|
|
53
|
+
try { fs.unlinkSync(dbPath + "-wal"); } catch { /* ignore */ }
|
|
54
|
+
try { fs.unlinkSync(dbPath + "-shm"); } catch { /* ignore */ }
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should create an acquisition case with correct tax deduction", () => {
|
|
58
|
+
// Create target company
|
|
59
|
+
const company = db.createCompany({
|
|
60
|
+
name: "亏损公司", industry: "零售", owner_name: "赵六",
|
|
61
|
+
owner_contact: "", status: "active", registered_capital: 500000, description: "",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const id = db.genId();
|
|
65
|
+
const now = new Date().toISOString();
|
|
66
|
+
const lossAmount = 200000;
|
|
67
|
+
const taxDeduction = calcTaxDeduction(lossAmount);
|
|
68
|
+
|
|
69
|
+
db.execute(
|
|
70
|
+
`INSERT INTO opc_acquisition_cases
|
|
71
|
+
(id, company_id, acquirer_id, case_type, status, trigger_reason,
|
|
72
|
+
acquisition_price, loss_amount, tax_deduction, initiated_date, notes, created_at, updated_at)
|
|
73
|
+
VALUES (?, ?, 'starriver', 'acquisition', 'evaluating', ?, ?, ?, ?, date('now'), ?, ?, ?)`,
|
|
74
|
+
id, company.id, "连续亏损",
|
|
75
|
+
100000, lossAmount, taxDeduction,
|
|
76
|
+
"", now, now,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const row = db.queryOne("SELECT * FROM opc_acquisition_cases WHERE id = ?", id) as Record<string, unknown>;
|
|
80
|
+
expect(row).not.toBeNull();
|
|
81
|
+
expect(row.loss_amount).toBe(200000);
|
|
82
|
+
expect(row.tax_deduction).toBe(50000); // 200000 × 25%
|
|
83
|
+
expect(row.acquisition_price).toBe(100000);
|
|
84
|
+
expect(row.status).toBe("evaluating");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should update company status to acquired when acquisition is created", () => {
|
|
88
|
+
const company = db.createCompany({
|
|
89
|
+
name: "被收购公司", industry: "IT", owner_name: "钱七",
|
|
90
|
+
owner_contact: "", status: "active", registered_capital: 300000, description: "",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const now = new Date().toISOString();
|
|
94
|
+
db.execute(
|
|
95
|
+
"UPDATE opc_companies SET status = 'acquired', updated_at = ? WHERE id = ?",
|
|
96
|
+
now, company.id,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const updated = db.getCompany(company.id);
|
|
100
|
+
expect(updated).not.toBeNull();
|
|
101
|
+
expect(updated!.status).toBe("acquired");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should compute acquisition summary correctly", () => {
|
|
105
|
+
const c1 = db.createCompany({ name: "C1", industry: "IT", owner_name: "X", owner_contact: "", status: "active", registered_capital: 0, description: "" });
|
|
106
|
+
const c2 = db.createCompany({ name: "C2", industry: "IT", owner_name: "Y", owner_contact: "", status: "active", registered_capital: 0, description: "" });
|
|
107
|
+
const now = new Date().toISOString();
|
|
108
|
+
|
|
109
|
+
// Create two acquisition cases
|
|
110
|
+
db.execute(
|
|
111
|
+
`INSERT INTO opc_acquisition_cases (id, company_id, acquirer_id, case_type, status, trigger_reason, acquisition_price, loss_amount, tax_deduction, created_at, updated_at)
|
|
112
|
+
VALUES (?, ?, 'starriver', 'acquisition', 'completed', '亏损', 50000, 100000, 25000, ?, ?)`,
|
|
113
|
+
db.genId(), c1.id, now, now,
|
|
114
|
+
);
|
|
115
|
+
db.execute(
|
|
116
|
+
`INSERT INTO opc_acquisition_cases (id, company_id, acquirer_id, case_type, status, trigger_reason, acquisition_price, loss_amount, tax_deduction, created_at, updated_at)
|
|
117
|
+
VALUES (?, ?, 'starriver', 'acquisition', 'evaluating', '市场萎缩', 80000, 200000, 50000, ?, ?)`,
|
|
118
|
+
db.genId(), c2.id, now, now,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const summary = db.queryOne(
|
|
122
|
+
`SELECT
|
|
123
|
+
COUNT(*) as total_cases,
|
|
124
|
+
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed,
|
|
125
|
+
COUNT(CASE WHEN status = 'evaluating' THEN 1 END) as evaluating,
|
|
126
|
+
COALESCE(SUM(acquisition_price), 0) as total_acquisition_cost,
|
|
127
|
+
COALESCE(SUM(loss_amount), 0) as total_loss_amount,
|
|
128
|
+
COALESCE(SUM(tax_deduction), 0) as total_tax_deduction
|
|
129
|
+
FROM opc_acquisition_cases`,
|
|
130
|
+
) as Record<string, number>;
|
|
131
|
+
|
|
132
|
+
expect(summary.total_cases).toBe(2);
|
|
133
|
+
expect(summary.completed).toBe(1);
|
|
134
|
+
expect(summary.evaluating).toBe(1);
|
|
135
|
+
expect(summary.total_acquisition_cost).toBe(130000);
|
|
136
|
+
expect(summary.total_loss_amount).toBe(300000);
|
|
137
|
+
expect(summary.total_tax_deduction).toBe(75000); // 100000×25% + 200000×25%
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { Type, type Static } from "@sinclair/typebox";
|
|
9
9
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
10
10
|
import type { OpcDatabase } from "../db/index.js";
|
|
11
|
-
import { json } from "../utils/tool-helper.js";
|
|
11
|
+
import { json, toolError } from "../utils/tool-helper.js";
|
|
12
12
|
|
|
13
13
|
const AcquisitionSchema = Type.Union([
|
|
14
14
|
Type.Object({
|
|
@@ -136,10 +136,10 @@ export function registerAcquisitionTool(api: OpenClawPluginApi, db: OpcDatabase)
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
default:
|
|
139
|
-
return
|
|
139
|
+
return toolError(`未知操作: ${(p as { action: string }).action}`, "UNKNOWN_ACTION");
|
|
140
140
|
}
|
|
141
141
|
} catch (err) {
|
|
142
|
-
return
|
|
142
|
+
return toolError(err instanceof Error ? err.message : String(err), "DB_ERROR");
|
|
143
143
|
}
|
|
144
144
|
},
|
|
145
145
|
},
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { Type, type Static } from "@sinclair/typebox";
|
|
10
10
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
11
11
|
import type { OpcDatabase } from "../db/index.js";
|
|
12
|
-
import { json } from "../utils/tool-helper.js";
|
|
12
|
+
import { json, toolError } from "../utils/tool-helper.js";
|
|
13
13
|
|
|
14
14
|
const AssetPackageSchema = Type.Union([
|
|
15
15
|
// ── 资产包管理 ──
|
|
@@ -149,7 +149,7 @@ export function registerAssetPackageTool(api: OpenClawPluginApi, db: OpcDatabase
|
|
|
149
149
|
|
|
150
150
|
case "get_package_detail": {
|
|
151
151
|
const pkg = db.queryOne("SELECT * FROM opc_asset_packages WHERE id = ?", p.package_id);
|
|
152
|
-
if (!pkg) return
|
|
152
|
+
if (!pkg) return toolError("资产包不存在", "RECORD_NOT_FOUND");
|
|
153
153
|
const items = db.query(
|
|
154
154
|
`SELECT i.*, c.name as company_name, c.industry, c.status as company_status
|
|
155
155
|
FROM opc_asset_package_items i
|
|
@@ -269,10 +269,10 @@ export function registerAssetPackageTool(api: OpenClawPluginApi, db: OpcDatabase
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
default:
|
|
272
|
-
return
|
|
272
|
+
return toolError(`未知操作: ${(p as { action: string }).action}`, "UNKNOWN_ACTION");
|
|
273
273
|
}
|
|
274
274
|
} catch (err) {
|
|
275
|
-
return
|
|
275
|
+
return toolError(err instanceof Error ? err.message : String(err), "DB_ERROR");
|
|
276
276
|
}
|
|
277
277
|
},
|
|
278
278
|
},
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — finance-tool 核心计算函数单元测试
|
|
3
|
+
*
|
|
4
|
+
* 由于 registerFinanceTool 依赖 OpenClaw Plugin API,这里直接测试纯函数逻辑。
|
|
5
|
+
* 通过导入模块文件并提取或复制核心计算函数来测试。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
|
|
10
|
+
// 复制 finance-tool.ts 中的纯计算函数进行测试
|
|
11
|
+
// 因为这些函数是模块内部的,无法直接导入
|
|
12
|
+
|
|
13
|
+
/** 小规模纳税人增值税简易计算 */
|
|
14
|
+
function calcVatSimple(salesAmount: number, rate = 0.03): { tax: number; rate: number } {
|
|
15
|
+
return { tax: Math.round(salesAmount * rate * 100) / 100, rate };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 企业所得税简算(小型微利企业优惠) */
|
|
19
|
+
function calcIncomeTax(profit: number): { tax: number; rate: number; note: string } {
|
|
20
|
+
if (profit <= 0) return { tax: 0, rate: 0, note: "无应纳税所得额" };
|
|
21
|
+
if (profit <= 3_000_000) {
|
|
22
|
+
const tax = Math.round(profit * 0.05 * 100) / 100;
|
|
23
|
+
return { tax, rate: 0.05, note: "小型微利企业优惠税率 5%" };
|
|
24
|
+
}
|
|
25
|
+
const tax = Math.round(profit * 0.25 * 100) / 100;
|
|
26
|
+
return { tax, rate: 0.25, note: "一般企业税率 25%" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("finance-tool calculations", () => {
|
|
30
|
+
describe("calcVatSimple — 增值税计算", () => {
|
|
31
|
+
it("should calculate VAT at default 3% rate", () => {
|
|
32
|
+
const result = calcVatSimple(100000);
|
|
33
|
+
expect(result.tax).toBe(3000);
|
|
34
|
+
expect(result.rate).toBe(0.03);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should calculate VAT at custom rate", () => {
|
|
38
|
+
const result = calcVatSimple(100000, 0.06);
|
|
39
|
+
expect(result.tax).toBe(6000);
|
|
40
|
+
expect(result.rate).toBe(0.06);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should handle zero sales", () => {
|
|
44
|
+
const result = calcVatSimple(0);
|
|
45
|
+
expect(result.tax).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should round to 2 decimal places", () => {
|
|
49
|
+
const result = calcVatSimple(33333.33);
|
|
50
|
+
// 33333.33 * 0.03 = 999.9999 → rounded to 1000.00
|
|
51
|
+
expect(result.tax).toBe(1000);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should handle small amounts correctly", () => {
|
|
55
|
+
const result = calcVatSimple(1);
|
|
56
|
+
expect(result.tax).toBe(0.03);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("calcIncomeTax — 企业所得税计算", () => {
|
|
61
|
+
it("should return 0 tax for zero profit", () => {
|
|
62
|
+
const result = calcIncomeTax(0);
|
|
63
|
+
expect(result.tax).toBe(0);
|
|
64
|
+
expect(result.rate).toBe(0);
|
|
65
|
+
expect(result.note).toBe("无应纳税所得额");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should return 0 tax for negative profit (亏损)", () => {
|
|
69
|
+
const result = calcIncomeTax(-100000);
|
|
70
|
+
expect(result.tax).toBe(0);
|
|
71
|
+
expect(result.rate).toBe(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should apply 5% rate for small-profit enterprises (≤300万)", () => {
|
|
75
|
+
const result = calcIncomeTax(1000000); // 100万利润
|
|
76
|
+
expect(result.tax).toBe(50000); // 100万 × 5% = 5万
|
|
77
|
+
expect(result.rate).toBe(0.05);
|
|
78
|
+
expect(result.note).toContain("5%");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should apply 5% rate at 300万 boundary", () => {
|
|
82
|
+
const result = calcIncomeTax(3000000); // 300万 boundary
|
|
83
|
+
expect(result.tax).toBe(150000); // 300万 × 5% = 15万
|
|
84
|
+
expect(result.rate).toBe(0.05);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should apply 25% rate for profits above 300万", () => {
|
|
88
|
+
const result = calcIncomeTax(5000000); // 500万利润
|
|
89
|
+
expect(result.tax).toBe(1250000); // 500万 × 25% = 125万
|
|
90
|
+
expect(result.rate).toBe(0.25);
|
|
91
|
+
expect(result.note).toContain("25%");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should handle small profit correctly", () => {
|
|
95
|
+
const result = calcIncomeTax(10000); // 1万利润
|
|
96
|
+
expect(result.tax).toBe(500); // 1万 × 5% = 500
|
|
97
|
+
expect(result.rate).toBe(0.05);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should round correctly for decimal profits", () => {
|
|
101
|
+
const result = calcIncomeTax(33333.33);
|
|
102
|
+
// 33333.33 * 0.05 = 1666.6665 → rounded to 1666.67
|
|
103
|
+
expect(result.tax).toBe(1666.67);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Type, type Static } from "@sinclair/typebox";
|
|
6
6
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
7
7
|
import type { OpcDatabase } from "../db/index.js";
|
|
8
|
-
import { json } from "../utils/tool-helper.js";
|
|
8
|
+
import { json, toolError } from "../utils/tool-helper.js";
|
|
9
9
|
|
|
10
10
|
const FinanceSchema = Type.Union([
|
|
11
11
|
Type.Object({
|
|
@@ -133,7 +133,9 @@ export function registerFinanceTool(api: OpenClawPluginApi, db: OpcDatabase): vo
|
|
|
133
133
|
|
|
134
134
|
case "update_invoice_status": {
|
|
135
135
|
db.execute("UPDATE opc_invoices SET status = ? WHERE id = ?", p.status, p.invoice_id);
|
|
136
|
-
|
|
136
|
+
const invoice = db.queryOne("SELECT * FROM opc_invoices WHERE id = ?", p.invoice_id);
|
|
137
|
+
if (!invoice) return toolError(`发票 ${p.invoice_id} 不存在`, "INVOICE_NOT_FOUND");
|
|
138
|
+
return json(invoice);
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
case "calc_vat": {
|
|
@@ -230,10 +232,10 @@ export function registerFinanceTool(api: OpenClawPluginApi, db: OpcDatabase): vo
|
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
default:
|
|
233
|
-
return
|
|
235
|
+
return toolError(`未知操作: ${(p as { action: string }).action}`, "UNKNOWN_ACTION");
|
|
234
236
|
}
|
|
235
237
|
} catch (err) {
|
|
236
|
-
return
|
|
238
|
+
return toolError(err instanceof Error ? err.message : String(err), "DB_ERROR");
|
|
237
239
|
}
|
|
238
240
|
},
|
|
239
241
|
},
|