galaxy-opc-plugin 0.2.1 → 0.2.3
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/README.md +207 -10
- package/index.ts +350 -8
- package/openclaw.plugin.json +1 -1
- package/package.json +17 -3
- package/src/__tests__/e2e/company-lifecycle.test.ts +399 -0
- package/src/__tests__/integration/business-workflows.test.ts +366 -0
- package/src/__tests__/test-utils.ts +316 -0
- package/src/api/companies.ts +4 -0
- package/src/api/dashboard.ts +368 -16
- package/src/api/routes.ts +2 -2
- package/src/commands/opc-command.ts +422 -0
- package/src/db/index.ts +3 -0
- package/src/db/migrations.test.ts +324 -0
- package/src/db/migrations.ts +277 -0
- package/src/db/schema.ts +312 -0
- package/src/db/sqlite-adapter.ts +44 -2
- package/src/opc/accounting-parser.ts +178 -0
- package/src/opc/autonomy-rules.ts +132 -0
- package/src/opc/briefing-builder.ts +1331 -0
- package/src/opc/business-workflows.test.ts +535 -0
- package/src/opc/business-workflows.ts +325 -0
- package/src/opc/context-injector.ts +366 -28
- package/src/opc/daily-brief.ts +529 -0
- package/src/opc/event-triggers.ts +472 -0
- package/src/opc/intelligence-engine.ts +702 -0
- package/src/opc/milestone-detector.ts +251 -0
- package/src/opc/onboarding-flow.ts +332 -0
- package/src/opc/proactive-service.ts +466 -0
- package/src/opc/reminder-service.ts +4 -43
- package/src/opc/session-task-tracker.ts +60 -0
- package/src/opc/stage-detector.ts +168 -0
- package/src/opc/task-executor.ts +332 -0
- package/src/opc/task-templates.ts +179 -0
- package/src/tools/document-tool.ts +1176 -0
- package/src/tools/finance-tool.test-payment.ts +326 -0
- package/src/tools/finance-tool.test.ts +238 -0
- package/src/tools/finance-tool.ts +1574 -14
- package/src/tools/hr-tool.ts +10 -1
- package/src/tools/legal-tool.test.ts +251 -0
- package/src/tools/legal-tool.ts +26 -4
- package/src/tools/lifecycle-tool.test.ts +231 -0
- package/src/tools/media-tool.ts +156 -1
- package/src/tools/monitoring-tool.ts +134 -1
- package/src/tools/onboarding-tool.ts +233 -0
- package/src/tools/opc-tool.test.ts +250 -0
- package/src/tools/opc-tool.ts +251 -28
- package/src/tools/order-tool.ts +481 -0
- package/src/tools/project-tool.test.ts +218 -0
- package/src/tools/schemas.ts +80 -0
- package/src/tools/search-tool.ts +227 -0
- package/src/tools/smart-accounting-tool.ts +144 -0
- package/src/tools/staff-tool.ts +395 -2
- package/src/web/DASHBOARD_INTEGRATION_GUIDE.md +478 -0
- package/src/web/config-ui-patches.ts +389 -0
- package/src/web/config-ui.ts +4162 -3555
- package/src/web/dashboard-ui.ts +582 -0
- package/src/web/landing-page.ts +56 -6
package/src/tools/hr-tool.ts
CHANGED
|
@@ -5,6 +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 { BusinessWorkflows } from "../opc/business-workflows.js";
|
|
8
9
|
import { json, toolError } from "../utils/tool-helper.js";
|
|
9
10
|
|
|
10
11
|
const HrSchema = Type.Union([
|
|
@@ -129,7 +130,15 @@ export function registerHrTool(api: OpenClawPluginApi, db: OpcDatabase): void {
|
|
|
129
130
|
p.contract_type ?? "full_time",
|
|
130
131
|
p.notes ?? "", now, now,
|
|
131
132
|
);
|
|
132
|
-
|
|
133
|
+
// 业务闭环:自动记里程碑
|
|
134
|
+
const workflows = new BusinessWorkflows(db);
|
|
135
|
+
const autoCreated = workflows.afterEmployeeAdded({
|
|
136
|
+
id, company_id: p.company_id, employee_name: p.employee_name,
|
|
137
|
+
position: p.position, contract_type: p.contract_type ?? "full_time",
|
|
138
|
+
salary: p.salary,
|
|
139
|
+
});
|
|
140
|
+
const record = db.queryOne("SELECT * FROM opc_hr_records WHERE id = ?", id);
|
|
141
|
+
return json({ ...record as object, _auto_created: autoCreated });
|
|
133
142
|
}
|
|
134
143
|
|
|
135
144
|
case "list_employees": {
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — legal-tool 集成测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
6
|
+
import { createTestDb, insertTestCompany, factories } from "../__tests__/test-utils.js";
|
|
7
|
+
import { SqliteAdapter } from "../db/sqlite-adapter.js";
|
|
8
|
+
|
|
9
|
+
describe("legal-tool database integration", () => {
|
|
10
|
+
let db: SqliteAdapter;
|
|
11
|
+
let companyId: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
db = createTestDb();
|
|
15
|
+
companyId = insertTestCompany(db);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
db.close();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("create_contract", () => {
|
|
23
|
+
it("should create a contract successfully", () => {
|
|
24
|
+
const contractData = factories.contract(companyId, {
|
|
25
|
+
title: "技术服务合同",
|
|
26
|
+
counterparty: "客户A",
|
|
27
|
+
contract_type: "service",
|
|
28
|
+
amount: 100000,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const id = db.genId();
|
|
32
|
+
const now = new Date().toISOString();
|
|
33
|
+
|
|
34
|
+
db.execute(
|
|
35
|
+
`INSERT INTO opc_contracts
|
|
36
|
+
(id, company_id, title, counterparty, contract_type, amount, start_date, end_date, status, key_terms, risk_notes, reminder_date, created_at, updated_at)
|
|
37
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
38
|
+
id, contractData.company_id, contractData.title, contractData.counterparty,
|
|
39
|
+
contractData.contract_type, contractData.amount, contractData.start_date,
|
|
40
|
+
contractData.end_date, contractData.status, contractData.key_terms,
|
|
41
|
+
contractData.risk_notes, contractData.reminder_date, now, now
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const contract = db.queryOne("SELECT * FROM opc_contracts WHERE id = ?", id) as any;
|
|
45
|
+
expect(contract).not.toBeNull();
|
|
46
|
+
expect(contract.title).toBe("技术服务合同");
|
|
47
|
+
expect(contract.amount).toBe(100000);
|
|
48
|
+
expect(contract.status).toBe("active");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should handle contract without amount", () => {
|
|
52
|
+
const contractData = factories.contract(companyId, {
|
|
53
|
+
title: "保密协议",
|
|
54
|
+
contract_type: "NDA",
|
|
55
|
+
amount: 0,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const id = db.genId();
|
|
59
|
+
const now = new Date().toISOString();
|
|
60
|
+
|
|
61
|
+
db.execute(
|
|
62
|
+
`INSERT INTO opc_contracts
|
|
63
|
+
(id, company_id, title, counterparty, contract_type, amount, start_date, end_date, status, key_terms, risk_notes, reminder_date, created_at, updated_at)
|
|
64
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
65
|
+
id, contractData.company_id, contractData.title, contractData.counterparty,
|
|
66
|
+
contractData.contract_type, contractData.amount, contractData.start_date,
|
|
67
|
+
contractData.end_date, contractData.status, contractData.key_terms,
|
|
68
|
+
contractData.risk_notes, contractData.reminder_date, now, now
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const contract = db.queryOne("SELECT * FROM opc_contracts WHERE id = ?", id) as any;
|
|
72
|
+
expect(contract.amount).toBe(0);
|
|
73
|
+
expect(contract.contract_type).toBe("NDA");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should create multiple contracts for same company", () => {
|
|
77
|
+
const contract1 = factories.contract(companyId, { title: "合同1" });
|
|
78
|
+
const contract2 = factories.contract(companyId, { title: "合同2" });
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
|
|
81
|
+
[contract1, contract2].forEach((c) => {
|
|
82
|
+
const id = db.genId();
|
|
83
|
+
db.execute(
|
|
84
|
+
`INSERT INTO opc_contracts
|
|
85
|
+
(id, company_id, title, counterparty, contract_type, amount, start_date, end_date, status, key_terms, risk_notes, reminder_date, created_at, updated_at)
|
|
86
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
87
|
+
id, c.company_id, c.title, c.counterparty, c.contract_type, c.amount,
|
|
88
|
+
c.start_date, c.end_date, c.status, c.key_terms, c.risk_notes, c.reminder_date, now, now
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const contracts = db.query("SELECT * FROM opc_contracts WHERE company_id = ?", companyId) as any[];
|
|
93
|
+
expect(contracts.length).toBe(2);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("list_contracts", () => {
|
|
98
|
+
it("should list all contracts for a company", () => {
|
|
99
|
+
// Create 3 contracts
|
|
100
|
+
for (let i = 0; i < 3; i++) {
|
|
101
|
+
const contract = factories.contract(companyId, { title: `合同${i + 1}` });
|
|
102
|
+
const id = db.genId();
|
|
103
|
+
const now = new Date().toISOString();
|
|
104
|
+
db.execute(
|
|
105
|
+
`INSERT INTO opc_contracts
|
|
106
|
+
(id, company_id, title, counterparty, contract_type, amount, start_date, end_date, status, key_terms, risk_notes, reminder_date, created_at, updated_at)
|
|
107
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
108
|
+
id, contract.company_id, contract.title, contract.counterparty,
|
|
109
|
+
contract.contract_type, contract.amount, contract.start_date,
|
|
110
|
+
contract.end_date, contract.status, contract.key_terms,
|
|
111
|
+
contract.risk_notes, contract.reminder_date, now, now
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const contracts = db.query("SELECT * FROM opc_contracts WHERE company_id = ?", companyId) as any[];
|
|
116
|
+
expect(contracts.length).toBe(3);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should filter contracts by status", () => {
|
|
120
|
+
const activeContract = factories.contract(companyId, { status: "active" });
|
|
121
|
+
const expiredContract = factories.contract(companyId, { status: "expired" });
|
|
122
|
+
const now = new Date().toISOString();
|
|
123
|
+
|
|
124
|
+
[activeContract, expiredContract].forEach((c) => {
|
|
125
|
+
const id = db.genId();
|
|
126
|
+
db.execute(
|
|
127
|
+
`INSERT INTO opc_contracts
|
|
128
|
+
(id, company_id, title, counterparty, contract_type, amount, start_date, end_date, status, key_terms, risk_notes, reminder_date, created_at, updated_at)
|
|
129
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
130
|
+
id, c.company_id, c.title, c.counterparty, c.contract_type, c.amount,
|
|
131
|
+
c.start_date, c.end_date, c.status, c.key_terms, c.risk_notes, c.reminder_date, now, now
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const activeContracts = db.query(
|
|
136
|
+
"SELECT * FROM opc_contracts WHERE company_id = ? AND status = ?",
|
|
137
|
+
companyId, "active"
|
|
138
|
+
) as any[];
|
|
139
|
+
expect(activeContracts.length).toBe(1);
|
|
140
|
+
expect(activeContracts[0].status).toBe("active");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("update_contract", () => {
|
|
145
|
+
it("should update contract status", () => {
|
|
146
|
+
const contract = factories.contract(companyId);
|
|
147
|
+
const id = db.genId();
|
|
148
|
+
const now = new Date().toISOString();
|
|
149
|
+
|
|
150
|
+
db.execute(
|
|
151
|
+
`INSERT INTO opc_contracts
|
|
152
|
+
(id, company_id, title, counterparty, contract_type, amount, start_date, end_date, status, key_terms, risk_notes, reminder_date, created_at, updated_at)
|
|
153
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
154
|
+
id, contract.company_id, contract.title, contract.counterparty,
|
|
155
|
+
contract.contract_type, contract.amount, contract.start_date,
|
|
156
|
+
contract.end_date, contract.status, contract.key_terms,
|
|
157
|
+
contract.risk_notes, contract.reminder_date, now, now
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
db.execute(
|
|
161
|
+
"UPDATE opc_contracts SET status = ?, updated_at = ? WHERE id = ?",
|
|
162
|
+
"terminated", now, id
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const updated = db.queryOne("SELECT * FROM opc_contracts WHERE id = ?", id) as any;
|
|
166
|
+
expect(updated.status).toBe("terminated");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should update risk notes", () => {
|
|
170
|
+
const contract = factories.contract(companyId);
|
|
171
|
+
const id = db.genId();
|
|
172
|
+
const now = new Date().toISOString();
|
|
173
|
+
|
|
174
|
+
db.execute(
|
|
175
|
+
`INSERT INTO opc_contracts
|
|
176
|
+
(id, company_id, title, counterparty, contract_type, amount, start_date, end_date, status, key_terms, risk_notes, reminder_date, created_at, updated_at)
|
|
177
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
178
|
+
id, contract.company_id, contract.title, contract.counterparty,
|
|
179
|
+
contract.contract_type, contract.amount, contract.start_date,
|
|
180
|
+
contract.end_date, contract.status, contract.key_terms,
|
|
181
|
+
contract.risk_notes, contract.reminder_date, now, now
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const newRiskNotes = "需注意付款条款";
|
|
185
|
+
db.execute(
|
|
186
|
+
"UPDATE opc_contracts SET risk_notes = ?, updated_at = ? WHERE id = ?",
|
|
187
|
+
newRiskNotes, now, id
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const updated = db.queryOne("SELECT * FROM opc_contracts WHERE id = ?", id) as any;
|
|
191
|
+
expect(updated.risk_notes).toBe(newRiskNotes);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("delete_contract", () => {
|
|
196
|
+
it("should delete a contract", () => {
|
|
197
|
+
const contract = factories.contract(companyId);
|
|
198
|
+
const id = db.genId();
|
|
199
|
+
const now = new Date().toISOString();
|
|
200
|
+
|
|
201
|
+
db.execute(
|
|
202
|
+
`INSERT INTO opc_contracts
|
|
203
|
+
(id, company_id, title, counterparty, contract_type, amount, start_date, end_date, status, key_terms, risk_notes, reminder_date, created_at, updated_at)
|
|
204
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
205
|
+
id, contract.company_id, contract.title, contract.counterparty,
|
|
206
|
+
contract.contract_type, contract.amount, contract.start_date,
|
|
207
|
+
contract.end_date, contract.status, contract.key_terms,
|
|
208
|
+
contract.risk_notes, contract.reminder_date, now, now
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
db.execute("DELETE FROM opc_contracts WHERE id = ?", id);
|
|
212
|
+
|
|
213
|
+
const deleted = db.queryOne("SELECT * FROM opc_contracts WHERE id = ?", id);
|
|
214
|
+
expect(deleted).toBeNull();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("contract expiration detection", () => {
|
|
219
|
+
it("should detect expiring contracts", () => {
|
|
220
|
+
const futureDate = new Date();
|
|
221
|
+
futureDate.setDate(futureDate.getDate() + 10);
|
|
222
|
+
const reminderDate = futureDate.toISOString().split("T")[0];
|
|
223
|
+
|
|
224
|
+
const contract = factories.contract(companyId, {
|
|
225
|
+
reminder_date: reminderDate,
|
|
226
|
+
status: "active",
|
|
227
|
+
});
|
|
228
|
+
const id = db.genId();
|
|
229
|
+
const now = new Date().toISOString();
|
|
230
|
+
|
|
231
|
+
db.execute(
|
|
232
|
+
`INSERT INTO opc_contracts
|
|
233
|
+
(id, company_id, title, counterparty, contract_type, amount, start_date, end_date, status, key_terms, risk_notes, reminder_date, created_at, updated_at)
|
|
234
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
235
|
+
id, contract.company_id, contract.title, contract.counterparty,
|
|
236
|
+
contract.contract_type, contract.amount, contract.start_date,
|
|
237
|
+
contract.end_date, contract.status, contract.key_terms,
|
|
238
|
+
contract.risk_notes, contract.reminder_date, now, now
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const expiringSoon = db.query(
|
|
242
|
+
`SELECT * FROM opc_contracts
|
|
243
|
+
WHERE company_id = ? AND status = 'active'
|
|
244
|
+
AND reminder_date <= date('now', '+30 days')`,
|
|
245
|
+
companyId
|
|
246
|
+
) as any[];
|
|
247
|
+
|
|
248
|
+
expect(expiringSoon.length).toBe(1);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
package/src/tools/legal-tool.ts
CHANGED
|
@@ -5,6 +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 { BusinessWorkflows, VALID_DIRECTIONS } from "../opc/business-workflows.js";
|
|
8
9
|
import { json, toolError } from "../utils/tool-helper.js";
|
|
9
10
|
|
|
10
11
|
const LegalSchema = Type.Union([
|
|
@@ -14,6 +15,9 @@ const LegalSchema = Type.Union([
|
|
|
14
15
|
title: Type.String({ description: "合同标题" }),
|
|
15
16
|
counterparty: Type.String({ description: "签约对方" }),
|
|
16
17
|
contract_type: Type.String({ description: "合同类型: 服务合同/采购合同/劳动合同/租赁合同/合作协议/NDA/其他" }),
|
|
18
|
+
direction: Type.Optional(Type.String({
|
|
19
|
+
description: "合同方向: sales(我方提供服务/产品,收钱) | procurement(我方采购,付钱) | outsourcing(外包/劳务) | partnership(合作协议)"
|
|
20
|
+
})),
|
|
17
21
|
amount: Type.Optional(Type.Number({ description: "合同金额(元)" })),
|
|
18
22
|
start_date: Type.Optional(Type.String({ description: "起始日期 (YYYY-MM-DD)" })),
|
|
19
23
|
end_date: Type.Optional(Type.String({ description: "结束日期 (YYYY-MM-DD)" })),
|
|
@@ -102,14 +106,32 @@ export function registerLegalTool(api: OpenClawPluginApi, db: OpcDatabase): void
|
|
|
102
106
|
case "create_contract": {
|
|
103
107
|
const id = db.genId();
|
|
104
108
|
const now = new Date().toISOString();
|
|
109
|
+
const direction = p.direction || "sales";
|
|
110
|
+
// 校验 direction 合法性
|
|
111
|
+
if (!BusinessWorkflows.validateDirection(direction)) {
|
|
112
|
+
return toolError(
|
|
113
|
+
`无效的合同方向「${direction}」,合法值: ${VALID_DIRECTIONS.join(", ")}`,
|
|
114
|
+
"INVALID_DIRECTION",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
105
117
|
db.execute(
|
|
106
|
-
`INSERT INTO opc_contracts (id, company_id, title, counterparty, contract_type, amount, start_date, end_date, status, key_terms, risk_notes, reminder_date, created_at, updated_at)
|
|
107
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'draft', ?, ?, ?, ?, ?)`,
|
|
108
|
-
id, p.company_id, p.title, p.counterparty, p.contract_type,
|
|
118
|
+
`INSERT INTO opc_contracts (id, company_id, title, counterparty, contract_type, direction, amount, start_date, end_date, status, key_terms, risk_notes, reminder_date, created_at, updated_at)
|
|
119
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'draft', ?, ?, ?, ?, ?)`,
|
|
120
|
+
id, p.company_id, p.title, p.counterparty, p.contract_type, direction,
|
|
109
121
|
p.amount ?? 0, p.start_date ?? "", p.end_date ?? "",
|
|
110
122
|
p.key_terms ?? "", p.risk_notes ?? "", p.reminder_date ?? "", now, now,
|
|
111
123
|
);
|
|
112
|
-
|
|
124
|
+
// 业务闭环:自动创建关联记录
|
|
125
|
+
const workflows = new BusinessWorkflows(db);
|
|
126
|
+
const autoCreated = workflows.afterContractCreated({
|
|
127
|
+
id, company_id: p.company_id, title: p.title,
|
|
128
|
+
counterparty: p.counterparty, contract_type: p.contract_type,
|
|
129
|
+
direction,
|
|
130
|
+
amount: p.amount ?? 0,
|
|
131
|
+
start_date: p.start_date ?? "", end_date: p.end_date ?? "",
|
|
132
|
+
});
|
|
133
|
+
const contract = db.queryOne("SELECT * FROM opc_contracts WHERE id = ?", id);
|
|
134
|
+
return json({ ...contract as object, _auto_created: autoCreated });
|
|
113
135
|
}
|
|
114
136
|
|
|
115
137
|
case "list_contracts": {
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星环OPC中心 — lifecycle-tool 集成测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
6
|
+
import { createTestDb, insertTestCompany } from "../__tests__/test-utils.js";
|
|
7
|
+
import { SqliteAdapter } from "../db/sqlite-adapter.js";
|
|
8
|
+
|
|
9
|
+
describe("lifecycle-tool database integration", () => {
|
|
10
|
+
let db: SqliteAdapter;
|
|
11
|
+
let companyId: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
db = createTestDb();
|
|
15
|
+
companyId = insertTestCompany(db);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
db.close();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("company status transitions", () => {
|
|
23
|
+
it("should transition from pending to active", () => {
|
|
24
|
+
const company = db.getCompany(companyId);
|
|
25
|
+
expect(company).not.toBeNull();
|
|
26
|
+
|
|
27
|
+
db.execute(
|
|
28
|
+
"UPDATE opc_companies SET status = ?, updated_at = ? WHERE id = ?",
|
|
29
|
+
"active", new Date().toISOString(), companyId
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const updated = db.getCompany(companyId);
|
|
33
|
+
expect(updated!.status).toBe("active");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should transition from active to suspended", () => {
|
|
37
|
+
db.execute(
|
|
38
|
+
"UPDATE opc_companies SET status = ?, updated_at = ? WHERE id = ?",
|
|
39
|
+
"suspended", new Date().toISOString(), companyId
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const updated = db.getCompany(companyId);
|
|
43
|
+
expect(updated!.status).toBe("suspended");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should transition from active to acquired", () => {
|
|
47
|
+
db.execute(
|
|
48
|
+
"UPDATE opc_companies SET status = ?, updated_at = ? WHERE id = ?",
|
|
49
|
+
"acquired", new Date().toISOString(), companyId
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const updated = db.getCompany(companyId);
|
|
53
|
+
expect(updated!.status).toBe("acquired");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should transition from acquired to packaged", () => {
|
|
57
|
+
db.execute(
|
|
58
|
+
"UPDATE opc_companies SET status = ?, updated_at = ? WHERE id = ?",
|
|
59
|
+
"packaged", new Date().toISOString(), companyId
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const updated = db.getCompany(companyId);
|
|
63
|
+
expect(updated!.status).toBe("packaged");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should transition to terminated", () => {
|
|
67
|
+
db.execute(
|
|
68
|
+
"UPDATE opc_companies SET status = ?, updated_at = ? WHERE id = ?",
|
|
69
|
+
"terminated", new Date().toISOString(), companyId
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const updated = db.getCompany(companyId);
|
|
73
|
+
expect(updated!.status).toBe("terminated");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("milestone tracking", () => {
|
|
78
|
+
it("should record first transaction milestone", () => {
|
|
79
|
+
const id = db.genId();
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
|
|
82
|
+
// Schema uses: title, category, target_date, completed_date, status
|
|
83
|
+
db.execute(
|
|
84
|
+
`INSERT INTO opc_milestones (id, company_id, title, category, description, completed_date, status, created_at)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
86
|
+
id, companyId, "首笔交易", "business", "首笔交易完成", "2026-01-15", "completed", now
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const milestones = db.query(
|
|
90
|
+
"SELECT * FROM opc_milestones WHERE company_id = ?",
|
|
91
|
+
companyId
|
|
92
|
+
) as any[];
|
|
93
|
+
expect(milestones.length).toBe(1);
|
|
94
|
+
expect(milestones[0].title).toBe("首笔交易");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should record profitability milestone", () => {
|
|
98
|
+
const id = db.genId();
|
|
99
|
+
const now = new Date().toISOString();
|
|
100
|
+
|
|
101
|
+
db.execute(
|
|
102
|
+
`INSERT INTO opc_milestones (id, company_id, title, category, description, completed_date, status, created_at)
|
|
103
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
104
|
+
id, companyId, "首次盈利", "financial", "实现首次盈利", "2026-02-01", "completed", now
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const milestones = db.query(
|
|
108
|
+
"SELECT * FROM opc_milestones WHERE company_id = ? AND title = ?",
|
|
109
|
+
companyId, "首次盈利"
|
|
110
|
+
) as any[];
|
|
111
|
+
expect(milestones.length).toBe(1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should track multiple milestones", () => {
|
|
115
|
+
const milestoneData = [
|
|
116
|
+
{ title: "首笔交易", category: "business" },
|
|
117
|
+
{ title: "首次盈利", category: "financial" },
|
|
118
|
+
{ title: "首位员工", category: "hr" },
|
|
119
|
+
];
|
|
120
|
+
const now = new Date().toISOString();
|
|
121
|
+
|
|
122
|
+
milestoneData.forEach((data) => {
|
|
123
|
+
const id = db.genId();
|
|
124
|
+
db.execute(
|
|
125
|
+
`INSERT INTO opc_milestones (id, company_id, title, category, description, completed_date, status, created_at)
|
|
126
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
127
|
+
id, companyId, data.title, data.category, `Milestone: ${data.title}`, "2026-01-15", "completed", now
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const milestones = db.query(
|
|
132
|
+
"SELECT * FROM opc_milestones WHERE company_id = ?",
|
|
133
|
+
companyId
|
|
134
|
+
) as any[];
|
|
135
|
+
expect(milestones.length).toBe(3);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("stage detection", () => {
|
|
140
|
+
it("should identify startup stage (new company)", () => {
|
|
141
|
+
const company = db.getCompany(companyId);
|
|
142
|
+
expect(company).not.toBeNull();
|
|
143
|
+
|
|
144
|
+
// No transactions yet - startup stage
|
|
145
|
+
const transactions = db.query(
|
|
146
|
+
"SELECT * FROM opc_transactions WHERE company_id = ?",
|
|
147
|
+
companyId
|
|
148
|
+
) as any[];
|
|
149
|
+
expect(transactions.length).toBe(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should identify growth stage (has revenue)", () => {
|
|
153
|
+
// Add revenue transactions
|
|
154
|
+
for (let i = 0; i < 3; i++) {
|
|
155
|
+
const id = db.genId();
|
|
156
|
+
db.execute(
|
|
157
|
+
`INSERT INTO opc_transactions (id, company_id, type, category, amount, description, counterparty, transaction_date, created_at)
|
|
158
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
159
|
+
id, companyId, "income", "service_income", 50000, "销售收入", "客户A", "2026-01-15"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const transactions = db.query(
|
|
164
|
+
"SELECT * FROM opc_transactions WHERE company_id = ? AND type = 'income'",
|
|
165
|
+
companyId
|
|
166
|
+
) as any[];
|
|
167
|
+
expect(transactions.length).toBeGreaterThan(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should identify maturity stage (consistent profit)", () => {
|
|
171
|
+
// Simulate 6 months of profitable transactions
|
|
172
|
+
for (let month = 1; month <= 6; month++) {
|
|
173
|
+
const incomeId = db.genId();
|
|
174
|
+
const expenseId = db.genId();
|
|
175
|
+
|
|
176
|
+
db.execute(
|
|
177
|
+
`INSERT INTO opc_transactions (id, company_id, type, category, amount, description, counterparty, transaction_date, created_at)
|
|
178
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
179
|
+
incomeId, companyId, "income", "service_income", 100000, "月收入", "客户", `2026-0${month}-15`
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
db.execute(
|
|
183
|
+
`INSERT INTO opc_transactions (id, company_id, type, category, amount, description, counterparty, transaction_date, created_at)
|
|
184
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
185
|
+
expenseId, companyId, "expense", "salary", 30000, "月支出", "员工", `2026-0${month}-20`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const summary = db.queryOne(
|
|
190
|
+
`SELECT
|
|
191
|
+
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as total_income,
|
|
192
|
+
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as total_expense
|
|
193
|
+
FROM opc_transactions WHERE company_id = ?`,
|
|
194
|
+
companyId
|
|
195
|
+
) as any;
|
|
196
|
+
|
|
197
|
+
expect(summary.total_income).toBeGreaterThan(summary.total_expense);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("health metrics", () => {
|
|
202
|
+
it("should calculate company health score", () => {
|
|
203
|
+
// Add some positive indicators
|
|
204
|
+
const incomeId = db.genId();
|
|
205
|
+
db.execute(
|
|
206
|
+
`INSERT INTO opc_transactions (id, company_id, type, category, amount, description, counterparty, transaction_date, created_at)
|
|
207
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
208
|
+
incomeId, companyId, "income", "service_income", 200000, "销售", "客户", "2026-01-15"
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const contractId = db.genId();
|
|
212
|
+
const now = new Date().toISOString();
|
|
213
|
+
db.execute(
|
|
214
|
+
`INSERT INTO opc_contracts (id, company_id, title, counterparty, contract_type, amount, start_date, end_date, status, key_terms, risk_notes, reminder_date, created_at, updated_at)
|
|
215
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
216
|
+
contractId, companyId, "服务合同", "客户A", "service", 100000, "2026-01-01", "2026-12-31", "active", "", "", "2026-11-30", now, now
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const metrics = db.queryOne(
|
|
220
|
+
`SELECT
|
|
221
|
+
(SELECT COUNT(*) FROM opc_transactions WHERE company_id = ? AND type = 'income') as income_count,
|
|
222
|
+
(SELECT COUNT(*) FROM opc_contracts WHERE company_id = ? AND status = 'active') as active_contracts
|
|
223
|
+
FROM opc_companies WHERE id = ?`,
|
|
224
|
+
companyId, companyId, companyId
|
|
225
|
+
) as any;
|
|
226
|
+
|
|
227
|
+
expect(metrics.income_count).toBeGreaterThan(0);
|
|
228
|
+
expect(metrics.active_contracts).toBeGreaterThan(0);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|