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.
Files changed (57) hide show
  1. package/README.md +207 -10
  2. package/index.ts +350 -8
  3. package/openclaw.plugin.json +1 -1
  4. package/package.json +17 -3
  5. package/src/__tests__/e2e/company-lifecycle.test.ts +399 -0
  6. package/src/__tests__/integration/business-workflows.test.ts +366 -0
  7. package/src/__tests__/test-utils.ts +316 -0
  8. package/src/api/companies.ts +4 -0
  9. package/src/api/dashboard.ts +368 -16
  10. package/src/api/routes.ts +2 -2
  11. package/src/commands/opc-command.ts +422 -0
  12. package/src/db/index.ts +3 -0
  13. package/src/db/migrations.test.ts +324 -0
  14. package/src/db/migrations.ts +277 -0
  15. package/src/db/schema.ts +312 -0
  16. package/src/db/sqlite-adapter.ts +44 -2
  17. package/src/opc/accounting-parser.ts +178 -0
  18. package/src/opc/autonomy-rules.ts +132 -0
  19. package/src/opc/briefing-builder.ts +1331 -0
  20. package/src/opc/business-workflows.test.ts +535 -0
  21. package/src/opc/business-workflows.ts +325 -0
  22. package/src/opc/context-injector.ts +366 -28
  23. package/src/opc/daily-brief.ts +529 -0
  24. package/src/opc/event-triggers.ts +472 -0
  25. package/src/opc/intelligence-engine.ts +702 -0
  26. package/src/opc/milestone-detector.ts +251 -0
  27. package/src/opc/onboarding-flow.ts +332 -0
  28. package/src/opc/proactive-service.ts +466 -0
  29. package/src/opc/reminder-service.ts +4 -43
  30. package/src/opc/session-task-tracker.ts +60 -0
  31. package/src/opc/stage-detector.ts +168 -0
  32. package/src/opc/task-executor.ts +332 -0
  33. package/src/opc/task-templates.ts +179 -0
  34. package/src/tools/document-tool.ts +1176 -0
  35. package/src/tools/finance-tool.test-payment.ts +326 -0
  36. package/src/tools/finance-tool.test.ts +238 -0
  37. package/src/tools/finance-tool.ts +1574 -14
  38. package/src/tools/hr-tool.ts +10 -1
  39. package/src/tools/legal-tool.test.ts +251 -0
  40. package/src/tools/legal-tool.ts +26 -4
  41. package/src/tools/lifecycle-tool.test.ts +231 -0
  42. package/src/tools/media-tool.ts +156 -1
  43. package/src/tools/monitoring-tool.ts +134 -1
  44. package/src/tools/onboarding-tool.ts +233 -0
  45. package/src/tools/opc-tool.test.ts +250 -0
  46. package/src/tools/opc-tool.ts +251 -28
  47. package/src/tools/order-tool.ts +481 -0
  48. package/src/tools/project-tool.test.ts +218 -0
  49. package/src/tools/schemas.ts +80 -0
  50. package/src/tools/search-tool.ts +227 -0
  51. package/src/tools/smart-accounting-tool.ts +144 -0
  52. package/src/tools/staff-tool.ts +395 -2
  53. package/src/web/DASHBOARD_INTEGRATION_GUIDE.md +478 -0
  54. package/src/web/config-ui-patches.ts +389 -0
  55. package/src/web/config-ui.ts +4162 -3555
  56. package/src/web/dashboard-ui.ts +582 -0
  57. package/src/web/landing-page.ts +56 -6
@@ -0,0 +1,324 @@
1
+ /**
2
+ * 星环OPC中心 — 数据库迁移和完整性测试
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
6
+ import { createTestDb } from "../__tests__/test-utils.js";
7
+ import { SqliteAdapter } from "./sqlite-adapter.js";
8
+
9
+ describe("database migrations", () => {
10
+ let db: SqliteAdapter;
11
+
12
+ beforeEach(() => {
13
+ db = createTestDb();
14
+ });
15
+
16
+ afterEach(() => {
17
+ db.close();
18
+ });
19
+
20
+ describe("table creation", () => {
21
+ it("should create all required tables", () => {
22
+ const tables = db.query(
23
+ "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
24
+ ) as any[];
25
+
26
+ const tableNames = tables.map((t) => t.name);
27
+
28
+ // Core tables
29
+ expect(tableNames).toContain("opc_companies");
30
+ expect(tableNames).toContain("opc_employees");
31
+ expect(tableNames).toContain("opc_transactions");
32
+ expect(tableNames).toContain("opc_contacts");
33
+
34
+ // Phase 2 tables
35
+ expect(tableNames).toContain("opc_contracts");
36
+ expect(tableNames).toContain("opc_invoices");
37
+ expect(tableNames).toContain("opc_projects");
38
+ expect(tableNames).toContain("opc_milestones");
39
+
40
+ // Extended tables
41
+ expect(tableNames).toContain("opc_tax_filings");
42
+ expect(tableNames).toContain("opc_media_content");
43
+ expect(tableNames).toContain("opc_acquisition_cases");
44
+ expect(tableNames).toContain("opc_asset_packages");
45
+ });
46
+
47
+ it("should have at least 25 tables", () => {
48
+ const tables = db.query(
49
+ "SELECT COUNT(*) as count FROM sqlite_master WHERE type='table'"
50
+ ) as any[];
51
+ expect(tables[0].count).toBeGreaterThanOrEqual(25);
52
+ });
53
+ });
54
+
55
+ describe("table schema validation", () => {
56
+ it("should have correct opc_companies schema", () => {
57
+ const schema = db.query(
58
+ "PRAGMA table_info(opc_companies)"
59
+ ) as any[];
60
+
61
+ const columns = schema.map((c) => c.name);
62
+ expect(columns).toContain("id");
63
+ expect(columns).toContain("name");
64
+ expect(columns).toContain("industry");
65
+ expect(columns).toContain("owner_name");
66
+ expect(columns).toContain("owner_contact");
67
+ expect(columns).toContain("status");
68
+ expect(columns).toContain("registered_capital");
69
+ expect(columns).toContain("description");
70
+ expect(columns).toContain("created_at");
71
+ expect(columns).toContain("updated_at");
72
+ });
73
+
74
+ it("should have correct opc_transactions schema", () => {
75
+ const schema = db.query(
76
+ "PRAGMA table_info(opc_transactions)"
77
+ ) as any[];
78
+
79
+ const columns = schema.map((c) => c.name);
80
+ expect(columns).toContain("id");
81
+ expect(columns).toContain("company_id");
82
+ expect(columns).toContain("type");
83
+ expect(columns).toContain("category");
84
+ expect(columns).toContain("amount");
85
+ expect(columns).toContain("description");
86
+ expect(columns).toContain("counterparty");
87
+ expect(columns).toContain("transaction_date");
88
+ expect(columns).toContain("created_at");
89
+ });
90
+
91
+ it("should have correct opc_contracts schema", () => {
92
+ const schema = db.query(
93
+ "PRAGMA table_info(opc_contracts)"
94
+ ) as any[];
95
+
96
+ const columns = schema.map((c) => c.name);
97
+ expect(columns).toContain("id");
98
+ expect(columns).toContain("company_id");
99
+ expect(columns).toContain("title");
100
+ expect(columns).toContain("counterparty");
101
+ expect(columns).toContain("contract_type");
102
+ expect(columns).toContain("amount");
103
+ expect(columns).toContain("status");
104
+ });
105
+
106
+ it("should have correct opc_projects schema", () => {
107
+ const schema = db.query(
108
+ "PRAGMA table_info(opc_projects)"
109
+ ) as any[];
110
+
111
+ const columns = schema.map((c) => c.name);
112
+ expect(columns).toContain("id");
113
+ expect(columns).toContain("company_id");
114
+ expect(columns).toContain("name");
115
+ expect(columns).toContain("status");
116
+ expect(columns).toContain("budget");
117
+ expect(columns).toContain("spent"); // Schema uses 'spent' not 'actual_cost'
118
+ // No 'progress' or 'priority' fields in current schema
119
+ });
120
+ });
121
+
122
+ describe("foreign key constraints", () => {
123
+ it("should have foreign keys enabled", () => {
124
+ const result = db.queryOne("PRAGMA foreign_keys") as any;
125
+ expect(result.foreign_keys).toBe(1);
126
+ });
127
+
128
+ it("should enforce foreign key on transactions", () => {
129
+ const id = db.genId();
130
+
131
+ // Try to insert transaction with non-existent company_id
132
+ expect(() => {
133
+ db.execute(
134
+ `INSERT INTO opc_transactions (id, company_id, type, category, amount, description, counterparty, transaction_date, created_at)
135
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
136
+ id, "non-existent-company-id", "income", "service_income", 10000, "test", "client", "2026-01-15"
137
+ );
138
+ }).toThrow();
139
+ });
140
+
141
+ it("should prevent deletion of company with related transactions", () => {
142
+ // Create company
143
+ const company = db.createCompany({
144
+ name: "测试公司",
145
+ industry: "IT",
146
+ owner_name: "张三",
147
+ owner_contact: "13800138000",
148
+ status: "active",
149
+ registered_capital: 100000,
150
+ description: "",
151
+ });
152
+
153
+ // Add transaction
154
+ const txId = db.genId();
155
+ db.execute(
156
+ `INSERT INTO opc_transactions (id, company_id, type, category, amount, description, counterparty, transaction_date, created_at)
157
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
158
+ txId, company.id, "income", "service_income", 10000, "test", "client", "2026-01-15"
159
+ );
160
+
161
+ // Verify transaction exists
162
+ let transactions = db.query(
163
+ "SELECT * FROM opc_transactions WHERE company_id = ?",
164
+ company.id
165
+ ) as any[];
166
+ expect(transactions.length).toBe(1);
167
+
168
+ // Try to delete company - should fail due to foreign key constraint
169
+ expect(() => {
170
+ db.execute("DELETE FROM opc_companies WHERE id = ?", company.id);
171
+ }).toThrow();
172
+
173
+ // Proper cleanup: delete related data first
174
+ db.execute("DELETE FROM opc_transactions WHERE company_id = ?", company.id);
175
+ db.execute("DELETE FROM opc_companies WHERE id = ?", company.id);
176
+
177
+ const deleted = db.getCompany(company.id);
178
+ expect(deleted).toBeNull();
179
+ });
180
+ });
181
+
182
+ describe("indexes", () => {
183
+ it("should have indexes on foreign keys", () => {
184
+ const indexes = db.query(
185
+ "SELECT name FROM sqlite_master WHERE type='index'"
186
+ ) as any[];
187
+
188
+ const indexNames = indexes.map((i) => i.name);
189
+
190
+ // Check for common foreign key indexes
191
+ expect(indexNames.some((name) => name.includes("company"))).toBe(true);
192
+ });
193
+ });
194
+
195
+ describe("data integrity", () => {
196
+ it("should enforce NOT NULL constraints", () => {
197
+ // Try to create company without required fields
198
+ expect(() => {
199
+ db.execute(
200
+ "INSERT INTO opc_companies (id) VALUES (?)",
201
+ db.genId()
202
+ );
203
+ }).toThrow();
204
+ });
205
+
206
+ it("should enforce unique constraints on id", () => {
207
+ const company = db.createCompany({
208
+ name: "公司1",
209
+ industry: "IT",
210
+ owner_name: "张三",
211
+ owner_contact: "13800138000",
212
+ status: "active",
213
+ registered_capital: 100000,
214
+ description: "",
215
+ });
216
+
217
+ // Try to insert another company with same id
218
+ const now = new Date().toISOString();
219
+ expect(() => {
220
+ db.execute(
221
+ `INSERT INTO opc_companies (id, name, industry, owner_name, owner_contact, status, registered_capital, description, created_at, updated_at)
222
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
223
+ company.id, "公司2", "IT", "李四", "13900139000", "active", 100000, "", now, now
224
+ );
225
+ }).toThrow();
226
+ });
227
+ });
228
+
229
+ describe("default values", () => {
230
+ it("should set default timestamps", () => {
231
+ const company = db.createCompany({
232
+ name: "测试公司",
233
+ industry: "IT",
234
+ owner_name: "张三",
235
+ owner_contact: "13800138000",
236
+ status: "active",
237
+ registered_capital: 100000,
238
+ description: "",
239
+ });
240
+
241
+ expect(company.created_at).toBeDefined();
242
+ expect(company.updated_at).toBeDefined();
243
+ expect(company.created_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
244
+ });
245
+ });
246
+
247
+ describe("transaction support", () => {
248
+ it("should support database transactions", () => {
249
+ const result = db.transaction(() => {
250
+ const company = db.createCompany({
251
+ name: "事务测试",
252
+ industry: "IT",
253
+ owner_name: "张三",
254
+ owner_contact: "13800138000",
255
+ status: "active",
256
+ registered_capital: 100000,
257
+ description: "",
258
+ });
259
+ return company.id;
260
+ });
261
+
262
+ expect(result).toBeDefined();
263
+ const company = db.getCompany(result);
264
+ expect(company).not.toBeNull();
265
+ });
266
+
267
+ it("should rollback on transaction error", () => {
268
+ const companiesBeforeCount = (db.query("SELECT COUNT(*) as count FROM opc_companies") as any[])[0].count;
269
+
270
+ expect(() => {
271
+ db.transaction(() => {
272
+ db.createCompany({
273
+ name: "将被回滚",
274
+ industry: "IT",
275
+ owner_name: "张三",
276
+ owner_contact: "13800138000",
277
+ status: "active",
278
+ registered_capital: 100000,
279
+ description: "",
280
+ });
281
+ throw new Error("Rollback test");
282
+ });
283
+ }).toThrow("Rollback test");
284
+
285
+ const companiesAfterCount = (db.query("SELECT COUNT(*) as count FROM opc_companies") as any[])[0].count;
286
+ expect(companiesAfterCount).toBe(companiesBeforeCount);
287
+ });
288
+ });
289
+
290
+ describe("migration idempotency", () => {
291
+ it("should handle multiple database instances", () => {
292
+ // Creating multiple instances should not cause errors
293
+ const db1 = createTestDb();
294
+ const db2 = createTestDb();
295
+
296
+ const company1 = db1.createCompany({
297
+ name: "DB1公司",
298
+ industry: "IT",
299
+ owner_name: "张三",
300
+ owner_contact: "13800138000",
301
+ status: "active",
302
+ registered_capital: 100000,
303
+ description: "",
304
+ });
305
+
306
+ const company2 = db2.createCompany({
307
+ name: "DB2公司",
308
+ industry: "IT",
309
+ owner_name: "李四",
310
+ owner_contact: "13900139000",
311
+ status: "active",
312
+ registered_capital: 100000,
313
+ description: "",
314
+ });
315
+
316
+ expect(company1.id).toBeDefined();
317
+ expect(company2.id).toBeDefined();
318
+ expect(company1.id).not.toBe(company2.id);
319
+
320
+ db1.close();
321
+ db2.close();
322
+ });
323
+ });
324
+ });
@@ -35,6 +35,283 @@ export const migrations: Migration[] = [
35
35
  // This migration exists as the OPB Canvas version marker.
36
36
  },
37
37
  },
38
+ {
39
+ version: 4,
40
+ description: "Proactive intelligence — insights, celebrations, company_stage, briefings",
41
+ up(_db) {
42
+ // Tables (opc_insights, opc_celebrations, opc_company_stage, opc_briefings)
43
+ // and indexes are created in initializeDatabase via OPC_TABLES/OPC_INDEXES.
44
+ // This migration exists as the proactive intelligence version marker.
45
+ },
46
+ },
47
+ {
48
+ version: 5,
49
+ description: "Staff tasks — AI employee work tracking and delegation",
50
+ up(_db) {
51
+ // opc_staff_tasks table created in initializeDatabase via OPC_TABLES/OPC_INDEXES.
52
+ },
53
+ },
54
+ {
55
+ version: 6,
56
+ description: "Add direction column to contracts for business workflow engine",
57
+ up(db) {
58
+ // 检查 direction 列是否已存在(新库由 schema.ts 创建时已包含)
59
+ const cols = db.pragma("table_info(opc_contracts)") as { name: string }[];
60
+ if (!cols.some(c => c.name === "direction")) {
61
+ db.exec("ALTER TABLE opc_contracts ADD COLUMN direction TEXT NOT NULL DEFAULT 'sales'");
62
+ }
63
+ },
64
+ },
65
+ {
66
+ version: 7,
67
+ description: "Add unique constraint on contacts (company_id, name) to prevent duplicates",
68
+ up(db) {
69
+ // 先清理已有重复数据(保留最早创建的那条)
70
+ db.exec(`
71
+ DELETE FROM opc_contacts WHERE rowid NOT IN (
72
+ SELECT MIN(rowid) FROM opc_contacts GROUP BY company_id, name
73
+ )
74
+ `);
75
+ // CREATE UNIQUE INDEX 本身是幂等的(IF NOT EXISTS)
76
+ db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_contacts_company_name ON opc_contacts(company_id, name)");
77
+ },
78
+ },
79
+ {
80
+ version: 8,
81
+ description: "Add task_type and schedule columns to staff_tasks for automated execution",
82
+ up(db) {
83
+ const cols = db.pragma("table_info(opc_staff_tasks)") as { name: string }[];
84
+ if (!cols.some(c => c.name === "task_type")) {
85
+ db.exec("ALTER TABLE opc_staff_tasks ADD COLUMN task_type TEXT NOT NULL DEFAULT 'manual'");
86
+ }
87
+ if (!cols.some(c => c.name === "schedule")) {
88
+ db.exec("ALTER TABLE opc_staff_tasks ADD COLUMN schedule TEXT NOT NULL DEFAULT 'on_demand'");
89
+ }
90
+ },
91
+ },
92
+ {
93
+ version: 9,
94
+ description: "Add session_key column to staff_tasks for subagent tracking",
95
+ up(db) {
96
+ const cols = db.pragma("table_info(opc_staff_tasks)") as { name: string }[];
97
+ if (!cols.some(c => c.name === "session_key")) {
98
+ db.exec("ALTER TABLE opc_staff_tasks ADD COLUMN session_key TEXT NOT NULL DEFAULT ''");
99
+ }
100
+ db.exec("CREATE INDEX IF NOT EXISTS idx_staff_tasks_session_key ON opc_staff_tasks(session_key)");
101
+ },
102
+ },
103
+ {
104
+ version: 10,
105
+ description: "CRM pipeline — add pipeline_stage, follow_up_date, deal_value, source to contacts + interactions table",
106
+ up(db) {
107
+ const cols = db.pragma("table_info(opc_contacts)") as { name: string }[];
108
+ if (!cols.some(c => c.name === "pipeline_stage")) {
109
+ db.exec("ALTER TABLE opc_contacts ADD COLUMN pipeline_stage TEXT NOT NULL DEFAULT 'lead'");
110
+ }
111
+ if (!cols.some(c => c.name === "follow_up_date")) {
112
+ db.exec("ALTER TABLE opc_contacts ADD COLUMN follow_up_date TEXT NOT NULL DEFAULT ''");
113
+ }
114
+ if (!cols.some(c => c.name === "deal_value")) {
115
+ db.exec("ALTER TABLE opc_contacts ADD COLUMN deal_value REAL NOT NULL DEFAULT 0");
116
+ }
117
+ if (!cols.some(c => c.name === "source")) {
118
+ db.exec("ALTER TABLE opc_contacts ADD COLUMN source TEXT NOT NULL DEFAULT ''");
119
+ }
120
+ // opc_contact_interactions table created in initializeDatabase via OPC_TABLES
121
+ db.exec("CREATE INDEX IF NOT EXISTS idx_interactions_contact ON opc_contact_interactions(contact_id)");
122
+ db.exec("CREATE INDEX IF NOT EXISTS idx_contacts_follow_up ON opc_contacts(follow_up_date)");
123
+ db.exec("CREATE INDEX IF NOT EXISTS idx_contacts_pipeline ON opc_contacts(pipeline_stage)");
124
+ },
125
+ },
126
+ {
127
+ version: 11,
128
+ description: "Document generation — opc_documents table",
129
+ up(_db) {
130
+ // opc_documents table created in initializeDatabase via OPC_TABLES
131
+ },
132
+ },
133
+ {
134
+ version: 12,
135
+ description: "Invoice enhancements — due_date column + invoice_items table",
136
+ up(db) {
137
+ const cols = db.pragma("table_info(opc_invoices)") as { name: string }[];
138
+ if (!cols.some(c => c.name === "due_date")) {
139
+ db.exec("ALTER TABLE opc_invoices ADD COLUMN due_date TEXT NOT NULL DEFAULT ''");
140
+ }
141
+ // opc_invoice_items table created in initializeDatabase via OPC_TABLES
142
+ db.exec("CREATE INDEX IF NOT EXISTS idx_invoice_items_invoice ON opc_invoice_items(invoice_id)");
143
+ },
144
+ },
145
+ {
146
+ version: 13,
147
+ description: "Content publishing — reviewer, review_notes, approved_at columns on media_content",
148
+ up(db) {
149
+ const cols = db.pragma("table_info(opc_media_content)") as { name: string }[];
150
+ if (!cols.some(c => c.name === "reviewer")) {
151
+ db.exec("ALTER TABLE opc_media_content ADD COLUMN reviewer TEXT NOT NULL DEFAULT ''");
152
+ }
153
+ if (!cols.some(c => c.name === "review_notes")) {
154
+ db.exec("ALTER TABLE opc_media_content ADD COLUMN review_notes TEXT NOT NULL DEFAULT ''");
155
+ }
156
+ if (!cols.some(c => c.name === "approved_at")) {
157
+ db.exec("ALTER TABLE opc_media_content ADD COLUMN approved_at TEXT NOT NULL DEFAULT ''");
158
+ }
159
+ },
160
+ },
161
+ {
162
+ version: 14,
163
+ description: "Financial reporting — financial_periods and payments tables for advanced analysis",
164
+ up(_db) {
165
+ // opc_financial_periods and opc_payments tables created in initializeDatabase via OPC_TABLES
166
+ // Indexes created via OPC_INDEXES
167
+ },
168
+ },
169
+ {
170
+ version: 15,
171
+ description: "Payment management enhancement — add direction, counterparty, paid_amount, category for receivables/payables",
172
+ up(db) {
173
+ const cols = db.pragma("table_info(opc_payments)") as { name: string }[];
174
+ if (!cols.some(c => c.name === "direction")) {
175
+ db.exec("ALTER TABLE opc_payments ADD COLUMN direction TEXT NOT NULL DEFAULT 'receivable'");
176
+ }
177
+ if (!cols.some(c => c.name === "counterparty")) {
178
+ db.exec("ALTER TABLE opc_payments ADD COLUMN counterparty TEXT NOT NULL DEFAULT ''");
179
+ }
180
+ if (!cols.some(c => c.name === "paid_amount")) {
181
+ db.exec("ALTER TABLE opc_payments ADD COLUMN paid_amount REAL NOT NULL DEFAULT 0");
182
+ }
183
+ if (!cols.some(c => c.name === "category")) {
184
+ db.exec("ALTER TABLE opc_payments ADD COLUMN category TEXT NOT NULL DEFAULT ''");
185
+ }
186
+ if (!cols.some(c => c.name === "payment_method")) {
187
+ db.exec("ALTER TABLE opc_payments ADD COLUMN payment_method TEXT NOT NULL DEFAULT ''");
188
+ }
189
+ if (!cols.some(c => c.name === "contract_id")) {
190
+ db.exec("ALTER TABLE opc_payments ADD COLUMN contract_id TEXT NOT NULL DEFAULT ''");
191
+ }
192
+ // 创建新索引
193
+ db.exec("CREATE INDEX IF NOT EXISTS idx_payments_direction ON opc_payments(direction)");
194
+ db.exec("CREATE INDEX IF NOT EXISTS idx_payments_counterparty ON opc_payments(counterparty)");
195
+ db.exec("CREATE INDEX IF NOT EXISTS idx_payments_company_direction ON opc_payments(company_id, direction)");
196
+ db.exec("CREATE INDEX IF NOT EXISTS idx_payments_company_status ON opc_payments(company_id, status)");
197
+ },
198
+ },
199
+ {
200
+ version: 16,
201
+ description: "Order workflow — quotations, quotation_items, contract_milestones tables",
202
+ up(db) {
203
+ // 报价单表
204
+ db.exec(`
205
+ CREATE TABLE IF NOT EXISTS opc_quotations (
206
+ id TEXT PRIMARY KEY,
207
+ company_id TEXT NOT NULL,
208
+ contact_id TEXT NOT NULL DEFAULT '',
209
+ quotation_number TEXT NOT NULL,
210
+ title TEXT NOT NULL,
211
+ total_amount REAL NOT NULL DEFAULT 0,
212
+ valid_until TEXT NOT NULL DEFAULT '',
213
+ status TEXT NOT NULL DEFAULT 'draft',
214
+ notes TEXT NOT NULL DEFAULT '',
215
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
216
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
217
+ FOREIGN KEY (company_id) REFERENCES opc_companies(id),
218
+ FOREIGN KEY (contact_id) REFERENCES opc_contacts(id)
219
+ )
220
+ `);
221
+
222
+ // 报价明细表
223
+ db.exec(`
224
+ CREATE TABLE IF NOT EXISTS opc_quotation_items (
225
+ id TEXT PRIMARY KEY,
226
+ quotation_id TEXT NOT NULL,
227
+ description TEXT NOT NULL,
228
+ quantity REAL NOT NULL DEFAULT 1,
229
+ unit_price REAL NOT NULL DEFAULT 0,
230
+ total_price REAL NOT NULL DEFAULT 0,
231
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
232
+ FOREIGN KEY (quotation_id) REFERENCES opc_quotations(id)
233
+ )
234
+ `);
235
+
236
+ // 合同里程碑表
237
+ db.exec(`
238
+ CREATE TABLE IF NOT EXISTS opc_contract_milestones (
239
+ id TEXT PRIMARY KEY,
240
+ contract_id TEXT NOT NULL,
241
+ company_id TEXT NOT NULL,
242
+ title TEXT NOT NULL,
243
+ description TEXT NOT NULL DEFAULT '',
244
+ due_date TEXT NOT NULL DEFAULT '',
245
+ amount REAL NOT NULL DEFAULT 0,
246
+ status TEXT NOT NULL DEFAULT 'pending',
247
+ completed_date TEXT NOT NULL DEFAULT '',
248
+ notes TEXT NOT NULL DEFAULT '',
249
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
250
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
251
+ FOREIGN KEY (contract_id) REFERENCES opc_contracts(id),
252
+ FOREIGN KEY (company_id) REFERENCES opc_companies(id)
253
+ )
254
+ `);
255
+
256
+ // 创建索引
257
+ db.exec("CREATE INDEX IF NOT EXISTS idx_quotations_company ON opc_quotations(company_id)");
258
+ db.exec("CREATE INDEX IF NOT EXISTS idx_quotations_status ON opc_quotations(status)");
259
+ db.exec("CREATE INDEX IF NOT EXISTS idx_quotations_contact ON opc_quotations(contact_id)");
260
+ db.exec("CREATE INDEX IF NOT EXISTS idx_quotation_items_quotation ON opc_quotation_items(quotation_id)");
261
+ db.exec("CREATE INDEX IF NOT EXISTS idx_contract_milestones_contract ON opc_contract_milestones(contract_id)");
262
+ db.exec("CREATE INDEX IF NOT EXISTS idx_contract_milestones_status ON opc_contract_milestones(status)");
263
+ db.exec("CREATE INDEX IF NOT EXISTS idx_contract_milestones_due_date ON opc_contract_milestones(due_date)");
264
+ },
265
+ },
266
+ {
267
+ version: 17,
268
+ description: "Contract enhancement — add quotation_id, signed_date, payment_terms",
269
+ up(db) {
270
+ const cols = db.pragma("table_info(opc_contracts)") as { name: string }[];
271
+
272
+ if (!cols.some(c => c.name === "quotation_id")) {
273
+ db.exec("ALTER TABLE opc_contracts ADD COLUMN quotation_id TEXT NOT NULL DEFAULT ''");
274
+ }
275
+ if (!cols.some(c => c.name === "signed_date")) {
276
+ db.exec("ALTER TABLE opc_contracts ADD COLUMN signed_date TEXT NOT NULL DEFAULT ''");
277
+ }
278
+ if (!cols.some(c => c.name === "payment_terms")) {
279
+ db.exec("ALTER TABLE opc_contracts ADD COLUMN payment_terms TEXT NOT NULL DEFAULT ''");
280
+ }
281
+
282
+ // 创建索引
283
+ db.exec("CREATE INDEX IF NOT EXISTS idx_contracts_quotation ON opc_contracts(quotation_id)");
284
+ db.exec("CREATE INDEX IF NOT EXISTS idx_contracts_signed_date ON opc_contracts(signed_date)");
285
+ },
286
+ },
287
+ {
288
+ version: 18,
289
+ description: "Payment enhancement — add milestone_id, overdue_days, risk_level for collection management",
290
+ up(db) {
291
+ const cols = db.pragma("table_info(opc_payments)") as { name: string }[];
292
+
293
+ if (!cols.some(c => c.name === "milestone_id")) {
294
+ db.exec("ALTER TABLE opc_payments ADD COLUMN milestone_id TEXT NOT NULL DEFAULT ''");
295
+ }
296
+ if (!cols.some(c => c.name === "overdue_days")) {
297
+ db.exec("ALTER TABLE opc_payments ADD COLUMN overdue_days INTEGER NOT NULL DEFAULT 0");
298
+ }
299
+ if (!cols.some(c => c.name === "risk_level")) {
300
+ db.exec("ALTER TABLE opc_payments ADD COLUMN risk_level TEXT NOT NULL DEFAULT 'normal'");
301
+ }
302
+ if (!cols.some(c => c.name === "last_remind_date")) {
303
+ db.exec("ALTER TABLE opc_payments ADD COLUMN last_remind_date TEXT NOT NULL DEFAULT ''");
304
+ }
305
+ if (!cols.some(c => c.name === "remind_count")) {
306
+ db.exec("ALTER TABLE opc_payments ADD COLUMN remind_count INTEGER NOT NULL DEFAULT 0");
307
+ }
308
+
309
+ // 创建索引
310
+ db.exec("CREATE INDEX IF NOT EXISTS idx_payments_milestone ON opc_payments(milestone_id)");
311
+ db.exec("CREATE INDEX IF NOT EXISTS idx_payments_risk_level ON opc_payments(risk_level)");
312
+ db.exec("CREATE INDEX IF NOT EXISTS idx_payments_overdue ON opc_payments(overdue_days)");
313
+ },
314
+ },
38
315
  ];
39
316
 
40
317
  /**