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.
@@ -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 json({ error: `未知操作: ${(p as { action: string }).action}` });
139
+ return toolError(`未知操作: ${(p as { action: string }).action}`, "UNKNOWN_ACTION");
140
140
  }
141
141
  } catch (err) {
142
- return json({ error: err instanceof Error ? err.message : String(err) });
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 json({ error: "资产包不存在" });
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 json({ error: `未知操作: ${(p as { action: string }).action}` });
272
+ return toolError(`未知操作: ${(p as { action: string }).action}`, "UNKNOWN_ACTION");
273
273
  }
274
274
  } catch (err) {
275
- return json({ error: err instanceof Error ? err.message : String(err) });
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
- return json(db.queryOne("SELECT * FROM opc_invoices WHERE id = ?", p.invoice_id) ?? { error: "发票不存在" });
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 json({ error: `未知操作: ${(p as { action: string }).action}` });
235
+ return toolError(`未知操作: ${(p as { action: string }).action}`, "UNKNOWN_ACTION");
234
236
  }
235
237
  } catch (err) {
236
- return json({ error: err instanceof Error ? err.message : String(err) });
238
+ return toolError(err instanceof Error ? err.message : String(err), "DB_ERROR");
237
239
  }
238
240
  },
239
241
  },