@hasna/microservices 0.0.4 → 0.0.6
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/bin/index.js +9 -1
- package/bin/mcp.js +9 -1
- package/dist/index.js +9 -1
- package/microservices/microservice-ads/src/cli/index.ts +198 -0
- package/microservices/microservice-ads/src/db/campaigns.ts +304 -0
- package/microservices/microservice-ads/src/mcp/index.ts +160 -0
- package/microservices/microservice-company/package.json +27 -0
- package/microservices/microservice-company/src/cli/index.ts +1126 -0
- package/microservices/microservice-company/src/db/company.ts +854 -0
- package/microservices/microservice-company/src/db/database.ts +93 -0
- package/microservices/microservice-company/src/db/migrations.ts +214 -0
- package/microservices/microservice-company/src/db/workflow-migrations.ts +44 -0
- package/microservices/microservice-company/src/index.ts +60 -0
- package/microservices/microservice-company/src/lib/audit.ts +168 -0
- package/microservices/microservice-company/src/lib/finance.ts +299 -0
- package/microservices/microservice-company/src/lib/settings.ts +85 -0
- package/microservices/microservice-company/src/lib/workflows.ts +698 -0
- package/microservices/microservice-company/src/mcp/index.ts +991 -0
- package/microservices/microservice-contracts/src/cli/index.ts +410 -23
- package/microservices/microservice-contracts/src/db/contracts.ts +430 -1
- package/microservices/microservice-contracts/src/db/migrations.ts +83 -0
- package/microservices/microservice-contracts/src/mcp/index.ts +312 -3
- package/microservices/microservice-domains/src/cli/index.ts +673 -0
- package/microservices/microservice-domains/src/db/domains.ts +613 -0
- package/microservices/microservice-domains/src/index.ts +21 -0
- package/microservices/microservice-domains/src/lib/brandsight.ts +285 -0
- package/microservices/microservice-domains/src/lib/godaddy.ts +328 -0
- package/microservices/microservice-domains/src/lib/namecheap.ts +474 -0
- package/microservices/microservice-domains/src/lib/registrar.ts +355 -0
- package/microservices/microservice-domains/src/mcp/index.ts +413 -0
- package/microservices/microservice-hiring/src/cli/index.ts +318 -8
- package/microservices/microservice-hiring/src/db/hiring.ts +503 -0
- package/microservices/microservice-hiring/src/db/migrations.ts +21 -0
- package/microservices/microservice-hiring/src/index.ts +29 -0
- package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
- package/microservices/microservice-hiring/src/mcp/index.ts +245 -0
- package/microservices/microservice-payments/src/cli/index.ts +255 -3
- package/microservices/microservice-payments/src/db/migrations.ts +18 -0
- package/microservices/microservice-payments/src/db/payments.ts +552 -0
- package/microservices/microservice-payments/src/mcp/index.ts +223 -0
- package/microservices/microservice-payroll/src/cli/index.ts +269 -0
- package/microservices/microservice-payroll/src/db/migrations.ts +26 -0
- package/microservices/microservice-payroll/src/db/payroll.ts +636 -0
- package/microservices/microservice-payroll/src/mcp/index.ts +246 -0
- package/microservices/microservice-shipping/src/cli/index.ts +211 -3
- package/microservices/microservice-shipping/src/db/migrations.ts +8 -0
- package/microservices/microservice-shipping/src/db/shipping.ts +453 -3
- package/microservices/microservice-shipping/src/mcp/index.ts +149 -1
- package/microservices/microservice-social/src/cli/index.ts +244 -2
- package/microservices/microservice-social/src/db/migrations.ts +33 -0
- package/microservices/microservice-social/src/db/social.ts +378 -4
- package/microservices/microservice-social/src/mcp/index.ts +221 -1
- package/microservices/microservice-subscriptions/src/cli/index.ts +315 -0
- package/microservices/microservice-subscriptions/src/db/migrations.ts +68 -0
- package/microservices/microservice-subscriptions/src/db/subscriptions.ts +567 -3
- package/microservices/microservice-subscriptions/src/mcp/index.ts +267 -1
- package/package.json +1 -1
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company CRUD operations — organizations, teams, members, customers, vendors
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDatabase } from "./database.js";
|
|
6
|
+
|
|
7
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export interface Organization {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
legal_name: string | null;
|
|
13
|
+
tax_id: string | null;
|
|
14
|
+
address: Record<string, unknown>;
|
|
15
|
+
phone: string | null;
|
|
16
|
+
email: string | null;
|
|
17
|
+
website: string | null;
|
|
18
|
+
industry: string | null;
|
|
19
|
+
currency: string;
|
|
20
|
+
fiscal_year_start: string;
|
|
21
|
+
timezone: string;
|
|
22
|
+
branding: Record<string, unknown>;
|
|
23
|
+
settings: Record<string, unknown>;
|
|
24
|
+
created_at: string;
|
|
25
|
+
updated_at: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface OrgRow {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
legal_name: string | null;
|
|
32
|
+
tax_id: string | null;
|
|
33
|
+
address: string;
|
|
34
|
+
phone: string | null;
|
|
35
|
+
email: string | null;
|
|
36
|
+
website: string | null;
|
|
37
|
+
industry: string | null;
|
|
38
|
+
currency: string;
|
|
39
|
+
fiscal_year_start: string;
|
|
40
|
+
timezone: string;
|
|
41
|
+
branding: string;
|
|
42
|
+
settings: string;
|
|
43
|
+
created_at: string;
|
|
44
|
+
updated_at: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Team {
|
|
48
|
+
id: string;
|
|
49
|
+
org_id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
parent_id: string | null;
|
|
52
|
+
department: string | null;
|
|
53
|
+
cost_center: string | null;
|
|
54
|
+
metadata: Record<string, unknown>;
|
|
55
|
+
created_at: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface TeamRow {
|
|
59
|
+
id: string;
|
|
60
|
+
org_id: string;
|
|
61
|
+
name: string;
|
|
62
|
+
parent_id: string | null;
|
|
63
|
+
department: string | null;
|
|
64
|
+
cost_center: string | null;
|
|
65
|
+
metadata: string;
|
|
66
|
+
created_at: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface Member {
|
|
70
|
+
id: string;
|
|
71
|
+
org_id: string;
|
|
72
|
+
team_id: string | null;
|
|
73
|
+
name: string;
|
|
74
|
+
email: string | null;
|
|
75
|
+
role: "owner" | "admin" | "manager" | "member" | "viewer";
|
|
76
|
+
title: string | null;
|
|
77
|
+
permissions: Record<string, unknown>;
|
|
78
|
+
status: string;
|
|
79
|
+
created_at: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface MemberRow {
|
|
83
|
+
id: string;
|
|
84
|
+
org_id: string;
|
|
85
|
+
team_id: string | null;
|
|
86
|
+
name: string;
|
|
87
|
+
email: string | null;
|
|
88
|
+
role: string;
|
|
89
|
+
title: string | null;
|
|
90
|
+
permissions: string;
|
|
91
|
+
status: string;
|
|
92
|
+
created_at: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface Customer {
|
|
96
|
+
id: string;
|
|
97
|
+
org_id: string;
|
|
98
|
+
name: string;
|
|
99
|
+
email: string | null;
|
|
100
|
+
phone: string | null;
|
|
101
|
+
company: string | null;
|
|
102
|
+
address: Record<string, unknown>;
|
|
103
|
+
source: string | null;
|
|
104
|
+
source_ids: Record<string, unknown>;
|
|
105
|
+
tags: string[];
|
|
106
|
+
lifetime_value: number;
|
|
107
|
+
metadata: Record<string, unknown>;
|
|
108
|
+
created_at: string;
|
|
109
|
+
updated_at: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface CustomerRow {
|
|
113
|
+
id: string;
|
|
114
|
+
org_id: string;
|
|
115
|
+
name: string;
|
|
116
|
+
email: string | null;
|
|
117
|
+
phone: string | null;
|
|
118
|
+
company: string | null;
|
|
119
|
+
address: string;
|
|
120
|
+
source: string | null;
|
|
121
|
+
source_ids: string;
|
|
122
|
+
tags: string;
|
|
123
|
+
lifetime_value: number;
|
|
124
|
+
metadata: string;
|
|
125
|
+
created_at: string;
|
|
126
|
+
updated_at: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface Vendor {
|
|
130
|
+
id: string;
|
|
131
|
+
org_id: string;
|
|
132
|
+
name: string;
|
|
133
|
+
email: string | null;
|
|
134
|
+
phone: string | null;
|
|
135
|
+
company: string | null;
|
|
136
|
+
category: "supplier" | "contractor" | "partner" | "agency" | null;
|
|
137
|
+
payment_terms: string | null;
|
|
138
|
+
address: Record<string, unknown>;
|
|
139
|
+
metadata: Record<string, unknown>;
|
|
140
|
+
created_at: string;
|
|
141
|
+
updated_at: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface VendorRow {
|
|
145
|
+
id: string;
|
|
146
|
+
org_id: string;
|
|
147
|
+
name: string;
|
|
148
|
+
email: string | null;
|
|
149
|
+
phone: string | null;
|
|
150
|
+
company: string | null;
|
|
151
|
+
category: string | null;
|
|
152
|
+
payment_terms: string | null;
|
|
153
|
+
address: string;
|
|
154
|
+
metadata: string;
|
|
155
|
+
created_at: string;
|
|
156
|
+
updated_at: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Row converters ──────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
function rowToOrg(row: OrgRow): Organization {
|
|
162
|
+
return {
|
|
163
|
+
...row,
|
|
164
|
+
address: JSON.parse(row.address || "{}"),
|
|
165
|
+
branding: JSON.parse(row.branding || "{}"),
|
|
166
|
+
settings: JSON.parse(row.settings || "{}"),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function rowToTeam(row: TeamRow): Team {
|
|
171
|
+
return {
|
|
172
|
+
...row,
|
|
173
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function rowToMember(row: MemberRow): Member {
|
|
178
|
+
return {
|
|
179
|
+
...row,
|
|
180
|
+
role: row.role as Member["role"],
|
|
181
|
+
permissions: JSON.parse(row.permissions || "{}"),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function rowToCustomer(row: CustomerRow): Customer {
|
|
186
|
+
return {
|
|
187
|
+
...row,
|
|
188
|
+
address: JSON.parse(row.address || "{}"),
|
|
189
|
+
source_ids: JSON.parse(row.source_ids || "{}"),
|
|
190
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
191
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function rowToVendor(row: VendorRow): Vendor {
|
|
196
|
+
return {
|
|
197
|
+
...row,
|
|
198
|
+
category: row.category as Vendor["category"],
|
|
199
|
+
address: JSON.parse(row.address || "{}"),
|
|
200
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─── Organizations ───────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
export interface CreateOrgInput {
|
|
207
|
+
name: string;
|
|
208
|
+
legal_name?: string;
|
|
209
|
+
tax_id?: string;
|
|
210
|
+
address?: Record<string, unknown>;
|
|
211
|
+
phone?: string;
|
|
212
|
+
email?: string;
|
|
213
|
+
website?: string;
|
|
214
|
+
industry?: string;
|
|
215
|
+
currency?: string;
|
|
216
|
+
fiscal_year_start?: string;
|
|
217
|
+
timezone?: string;
|
|
218
|
+
branding?: Record<string, unknown>;
|
|
219
|
+
settings?: Record<string, unknown>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function createOrg(input: CreateOrgInput): Organization {
|
|
223
|
+
const db = getDatabase();
|
|
224
|
+
const id = crypto.randomUUID();
|
|
225
|
+
|
|
226
|
+
db.prepare(
|
|
227
|
+
`INSERT INTO organizations (id, name, legal_name, tax_id, address, phone, email, website, industry, currency, fiscal_year_start, timezone, branding, settings)
|
|
228
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
229
|
+
).run(
|
|
230
|
+
id,
|
|
231
|
+
input.name,
|
|
232
|
+
input.legal_name || null,
|
|
233
|
+
input.tax_id || null,
|
|
234
|
+
JSON.stringify(input.address || {}),
|
|
235
|
+
input.phone || null,
|
|
236
|
+
input.email || null,
|
|
237
|
+
input.website || null,
|
|
238
|
+
input.industry || null,
|
|
239
|
+
input.currency || "USD",
|
|
240
|
+
input.fiscal_year_start || "01-01",
|
|
241
|
+
input.timezone || "UTC",
|
|
242
|
+
JSON.stringify(input.branding || {}),
|
|
243
|
+
JSON.stringify(input.settings || {})
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
return getOrg(id)!;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function getOrg(id: string): Organization | null {
|
|
250
|
+
const db = getDatabase();
|
|
251
|
+
const row = db.prepare("SELECT * FROM organizations WHERE id = ?").get(id) as OrgRow | null;
|
|
252
|
+
return row ? rowToOrg(row) : null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface UpdateOrgInput {
|
|
256
|
+
name?: string;
|
|
257
|
+
legal_name?: string;
|
|
258
|
+
tax_id?: string;
|
|
259
|
+
address?: Record<string, unknown>;
|
|
260
|
+
phone?: string;
|
|
261
|
+
email?: string;
|
|
262
|
+
website?: string;
|
|
263
|
+
industry?: string;
|
|
264
|
+
currency?: string;
|
|
265
|
+
fiscal_year_start?: string;
|
|
266
|
+
timezone?: string;
|
|
267
|
+
branding?: Record<string, unknown>;
|
|
268
|
+
settings?: Record<string, unknown>;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function updateOrg(id: string, input: UpdateOrgInput): Organization | null {
|
|
272
|
+
const db = getDatabase();
|
|
273
|
+
const existing = getOrg(id);
|
|
274
|
+
if (!existing) return null;
|
|
275
|
+
|
|
276
|
+
const sets: string[] = [];
|
|
277
|
+
const params: unknown[] = [];
|
|
278
|
+
|
|
279
|
+
if (input.name !== undefined) { sets.push("name = ?"); params.push(input.name); }
|
|
280
|
+
if (input.legal_name !== undefined) { sets.push("legal_name = ?"); params.push(input.legal_name); }
|
|
281
|
+
if (input.tax_id !== undefined) { sets.push("tax_id = ?"); params.push(input.tax_id); }
|
|
282
|
+
if (input.address !== undefined) { sets.push("address = ?"); params.push(JSON.stringify(input.address)); }
|
|
283
|
+
if (input.phone !== undefined) { sets.push("phone = ?"); params.push(input.phone); }
|
|
284
|
+
if (input.email !== undefined) { sets.push("email = ?"); params.push(input.email); }
|
|
285
|
+
if (input.website !== undefined) { sets.push("website = ?"); params.push(input.website); }
|
|
286
|
+
if (input.industry !== undefined) { sets.push("industry = ?"); params.push(input.industry); }
|
|
287
|
+
if (input.currency !== undefined) { sets.push("currency = ?"); params.push(input.currency); }
|
|
288
|
+
if (input.fiscal_year_start !== undefined) { sets.push("fiscal_year_start = ?"); params.push(input.fiscal_year_start); }
|
|
289
|
+
if (input.timezone !== undefined) { sets.push("timezone = ?"); params.push(input.timezone); }
|
|
290
|
+
if (input.branding !== undefined) { sets.push("branding = ?"); params.push(JSON.stringify(input.branding)); }
|
|
291
|
+
if (input.settings !== undefined) { sets.push("settings = ?"); params.push(JSON.stringify(input.settings)); }
|
|
292
|
+
|
|
293
|
+
if (sets.length === 0) return existing;
|
|
294
|
+
|
|
295
|
+
sets.push("updated_at = datetime('now')");
|
|
296
|
+
params.push(id);
|
|
297
|
+
|
|
298
|
+
db.prepare(`UPDATE organizations SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
299
|
+
|
|
300
|
+
return getOrg(id);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ─── Teams ───────────────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
export interface CreateTeamInput {
|
|
306
|
+
org_id: string;
|
|
307
|
+
name: string;
|
|
308
|
+
parent_id?: string;
|
|
309
|
+
department?: string;
|
|
310
|
+
cost_center?: string;
|
|
311
|
+
metadata?: Record<string, unknown>;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function createTeam(input: CreateTeamInput): Team {
|
|
315
|
+
const db = getDatabase();
|
|
316
|
+
const id = crypto.randomUUID();
|
|
317
|
+
|
|
318
|
+
db.prepare(
|
|
319
|
+
`INSERT INTO teams (id, org_id, name, parent_id, department, cost_center, metadata)
|
|
320
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
321
|
+
).run(
|
|
322
|
+
id,
|
|
323
|
+
input.org_id,
|
|
324
|
+
input.name,
|
|
325
|
+
input.parent_id || null,
|
|
326
|
+
input.department || null,
|
|
327
|
+
input.cost_center || null,
|
|
328
|
+
JSON.stringify(input.metadata || {})
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
return getTeam(id)!;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function getTeam(id: string): Team | null {
|
|
335
|
+
const db = getDatabase();
|
|
336
|
+
const row = db.prepare("SELECT * FROM teams WHERE id = ?").get(id) as TeamRow | null;
|
|
337
|
+
return row ? rowToTeam(row) : null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export interface ListTeamsOptions {
|
|
341
|
+
org_id?: string;
|
|
342
|
+
department?: string;
|
|
343
|
+
parent_id?: string | null;
|
|
344
|
+
limit?: number;
|
|
345
|
+
offset?: number;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function listTeams(options: ListTeamsOptions = {}): Team[] {
|
|
349
|
+
const db = getDatabase();
|
|
350
|
+
const conditions: string[] = [];
|
|
351
|
+
const params: unknown[] = [];
|
|
352
|
+
|
|
353
|
+
if (options.org_id) { conditions.push("org_id = ?"); params.push(options.org_id); }
|
|
354
|
+
if (options.department) { conditions.push("department = ?"); params.push(options.department); }
|
|
355
|
+
if (options.parent_id !== undefined) {
|
|
356
|
+
if (options.parent_id === null) {
|
|
357
|
+
conditions.push("parent_id IS NULL");
|
|
358
|
+
} else {
|
|
359
|
+
conditions.push("parent_id = ?");
|
|
360
|
+
params.push(options.parent_id);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let sql = "SELECT * FROM teams";
|
|
365
|
+
if (conditions.length > 0) sql += " WHERE " + conditions.join(" AND ");
|
|
366
|
+
sql += " ORDER BY name";
|
|
367
|
+
|
|
368
|
+
if (options.limit) { sql += " LIMIT ?"; params.push(options.limit); }
|
|
369
|
+
if (options.offset) { sql += " OFFSET ?"; params.push(options.offset); }
|
|
370
|
+
|
|
371
|
+
const rows = db.prepare(sql).all(...params) as TeamRow[];
|
|
372
|
+
return rows.map(rowToTeam);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export interface UpdateTeamInput {
|
|
376
|
+
name?: string;
|
|
377
|
+
parent_id?: string | null;
|
|
378
|
+
department?: string;
|
|
379
|
+
cost_center?: string;
|
|
380
|
+
metadata?: Record<string, unknown>;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function updateTeam(id: string, input: UpdateTeamInput): Team | null {
|
|
384
|
+
const db = getDatabase();
|
|
385
|
+
const existing = getTeam(id);
|
|
386
|
+
if (!existing) return null;
|
|
387
|
+
|
|
388
|
+
const sets: string[] = [];
|
|
389
|
+
const params: unknown[] = [];
|
|
390
|
+
|
|
391
|
+
if (input.name !== undefined) { sets.push("name = ?"); params.push(input.name); }
|
|
392
|
+
if (input.parent_id !== undefined) { sets.push("parent_id = ?"); params.push(input.parent_id); }
|
|
393
|
+
if (input.department !== undefined) { sets.push("department = ?"); params.push(input.department); }
|
|
394
|
+
if (input.cost_center !== undefined) { sets.push("cost_center = ?"); params.push(input.cost_center); }
|
|
395
|
+
if (input.metadata !== undefined) { sets.push("metadata = ?"); params.push(JSON.stringify(input.metadata)); }
|
|
396
|
+
|
|
397
|
+
if (sets.length === 0) return existing;
|
|
398
|
+
|
|
399
|
+
params.push(id);
|
|
400
|
+
db.prepare(`UPDATE teams SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
401
|
+
|
|
402
|
+
return getTeam(id);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function deleteTeam(id: string): boolean {
|
|
406
|
+
const db = getDatabase();
|
|
407
|
+
const result = db.prepare("DELETE FROM teams WHERE id = ?").run(id);
|
|
408
|
+
return result.changes > 0;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export interface TeamTreeNode extends Team {
|
|
412
|
+
children: TeamTreeNode[];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function getTeamTree(orgId: string): TeamTreeNode[] {
|
|
416
|
+
const allTeams = listTeams({ org_id: orgId });
|
|
417
|
+
const map = new Map<string, TeamTreeNode>();
|
|
418
|
+
|
|
419
|
+
// Create nodes
|
|
420
|
+
for (const team of allTeams) {
|
|
421
|
+
map.set(team.id, { ...team, children: [] });
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Build tree
|
|
425
|
+
const roots: TeamTreeNode[] = [];
|
|
426
|
+
for (const node of map.values()) {
|
|
427
|
+
if (node.parent_id && map.has(node.parent_id)) {
|
|
428
|
+
map.get(node.parent_id)!.children.push(node);
|
|
429
|
+
} else {
|
|
430
|
+
roots.push(node);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return roots;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function getTeamMembers(teamId: string): Member[] {
|
|
438
|
+
const db = getDatabase();
|
|
439
|
+
const rows = db.prepare("SELECT * FROM members WHERE team_id = ? ORDER BY name").all(teamId) as MemberRow[];
|
|
440
|
+
return rows.map(rowToMember);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ─── Members ─────────────────────────────────────────────────────────────────
|
|
444
|
+
|
|
445
|
+
export interface AddMemberInput {
|
|
446
|
+
org_id: string;
|
|
447
|
+
team_id?: string;
|
|
448
|
+
name: string;
|
|
449
|
+
email?: string;
|
|
450
|
+
role?: "owner" | "admin" | "manager" | "member" | "viewer";
|
|
451
|
+
title?: string;
|
|
452
|
+
permissions?: Record<string, unknown>;
|
|
453
|
+
status?: string;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function addMember(input: AddMemberInput): Member {
|
|
457
|
+
const db = getDatabase();
|
|
458
|
+
const id = crypto.randomUUID();
|
|
459
|
+
|
|
460
|
+
db.prepare(
|
|
461
|
+
`INSERT INTO members (id, org_id, team_id, name, email, role, title, permissions, status)
|
|
462
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
463
|
+
).run(
|
|
464
|
+
id,
|
|
465
|
+
input.org_id,
|
|
466
|
+
input.team_id || null,
|
|
467
|
+
input.name,
|
|
468
|
+
input.email || null,
|
|
469
|
+
input.role || "member",
|
|
470
|
+
input.title || null,
|
|
471
|
+
JSON.stringify(input.permissions || {}),
|
|
472
|
+
input.status || "active"
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
return getMember(id)!;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function getMember(id: string): Member | null {
|
|
479
|
+
const db = getDatabase();
|
|
480
|
+
const row = db.prepare("SELECT * FROM members WHERE id = ?").get(id) as MemberRow | null;
|
|
481
|
+
return row ? rowToMember(row) : null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export interface ListMembersOptions {
|
|
485
|
+
org_id?: string;
|
|
486
|
+
team_id?: string;
|
|
487
|
+
role?: string;
|
|
488
|
+
status?: string;
|
|
489
|
+
limit?: number;
|
|
490
|
+
offset?: number;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function listMembers(options: ListMembersOptions = {}): Member[] {
|
|
494
|
+
const db = getDatabase();
|
|
495
|
+
const conditions: string[] = [];
|
|
496
|
+
const params: unknown[] = [];
|
|
497
|
+
|
|
498
|
+
if (options.org_id) { conditions.push("org_id = ?"); params.push(options.org_id); }
|
|
499
|
+
if (options.team_id) { conditions.push("team_id = ?"); params.push(options.team_id); }
|
|
500
|
+
if (options.role) { conditions.push("role = ?"); params.push(options.role); }
|
|
501
|
+
if (options.status) { conditions.push("status = ?"); params.push(options.status); }
|
|
502
|
+
|
|
503
|
+
let sql = "SELECT * FROM members";
|
|
504
|
+
if (conditions.length > 0) sql += " WHERE " + conditions.join(" AND ");
|
|
505
|
+
sql += " ORDER BY name";
|
|
506
|
+
|
|
507
|
+
if (options.limit) { sql += " LIMIT ?"; params.push(options.limit); }
|
|
508
|
+
if (options.offset) { sql += " OFFSET ?"; params.push(options.offset); }
|
|
509
|
+
|
|
510
|
+
const rows = db.prepare(sql).all(...params) as MemberRow[];
|
|
511
|
+
return rows.map(rowToMember);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export interface UpdateMemberInput {
|
|
515
|
+
team_id?: string | null;
|
|
516
|
+
name?: string;
|
|
517
|
+
email?: string;
|
|
518
|
+
role?: "owner" | "admin" | "manager" | "member" | "viewer";
|
|
519
|
+
title?: string;
|
|
520
|
+
permissions?: Record<string, unknown>;
|
|
521
|
+
status?: string;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function updateMember(id: string, input: UpdateMemberInput): Member | null {
|
|
525
|
+
const db = getDatabase();
|
|
526
|
+
const existing = getMember(id);
|
|
527
|
+
if (!existing) return null;
|
|
528
|
+
|
|
529
|
+
const sets: string[] = [];
|
|
530
|
+
const params: unknown[] = [];
|
|
531
|
+
|
|
532
|
+
if (input.team_id !== undefined) { sets.push("team_id = ?"); params.push(input.team_id); }
|
|
533
|
+
if (input.name !== undefined) { sets.push("name = ?"); params.push(input.name); }
|
|
534
|
+
if (input.email !== undefined) { sets.push("email = ?"); params.push(input.email); }
|
|
535
|
+
if (input.role !== undefined) { sets.push("role = ?"); params.push(input.role); }
|
|
536
|
+
if (input.title !== undefined) { sets.push("title = ?"); params.push(input.title); }
|
|
537
|
+
if (input.permissions !== undefined) { sets.push("permissions = ?"); params.push(JSON.stringify(input.permissions)); }
|
|
538
|
+
if (input.status !== undefined) { sets.push("status = ?"); params.push(input.status); }
|
|
539
|
+
|
|
540
|
+
if (sets.length === 0) return existing;
|
|
541
|
+
|
|
542
|
+
params.push(id);
|
|
543
|
+
db.prepare(`UPDATE members SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
544
|
+
|
|
545
|
+
return getMember(id);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function removeMember(id: string): boolean {
|
|
549
|
+
const db = getDatabase();
|
|
550
|
+
const result = db.prepare("DELETE FROM members WHERE id = ?").run(id);
|
|
551
|
+
return result.changes > 0;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function getMembersByRole(orgId: string, role: string): Member[] {
|
|
555
|
+
return listMembers({ org_id: orgId, role });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function getMembersByTeam(teamId: string): Member[] {
|
|
559
|
+
return listMembers({ team_id: teamId });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ─── Customers ───────────────────────────────────────────────────────────────
|
|
563
|
+
|
|
564
|
+
export interface CreateCustomerInput {
|
|
565
|
+
org_id: string;
|
|
566
|
+
name: string;
|
|
567
|
+
email?: string;
|
|
568
|
+
phone?: string;
|
|
569
|
+
company?: string;
|
|
570
|
+
address?: Record<string, unknown>;
|
|
571
|
+
source?: string;
|
|
572
|
+
source_ids?: Record<string, unknown>;
|
|
573
|
+
tags?: string[];
|
|
574
|
+
lifetime_value?: number;
|
|
575
|
+
metadata?: Record<string, unknown>;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function createCustomer(input: CreateCustomerInput): Customer {
|
|
579
|
+
const db = getDatabase();
|
|
580
|
+
const id = crypto.randomUUID();
|
|
581
|
+
|
|
582
|
+
db.prepare(
|
|
583
|
+
`INSERT INTO customers (id, org_id, name, email, phone, company, address, source, source_ids, tags, lifetime_value, metadata)
|
|
584
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
585
|
+
).run(
|
|
586
|
+
id,
|
|
587
|
+
input.org_id,
|
|
588
|
+
input.name,
|
|
589
|
+
input.email || null,
|
|
590
|
+
input.phone || null,
|
|
591
|
+
input.company || null,
|
|
592
|
+
JSON.stringify(input.address || {}),
|
|
593
|
+
input.source || null,
|
|
594
|
+
JSON.stringify(input.source_ids || {}),
|
|
595
|
+
JSON.stringify(input.tags || []),
|
|
596
|
+
input.lifetime_value ?? 0,
|
|
597
|
+
JSON.stringify(input.metadata || {})
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
return getCustomer(id)!;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export function getCustomer(id: string): Customer | null {
|
|
604
|
+
const db = getDatabase();
|
|
605
|
+
const row = db.prepare("SELECT * FROM customers WHERE id = ?").get(id) as CustomerRow | null;
|
|
606
|
+
return row ? rowToCustomer(row) : null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export interface ListCustomersOptions {
|
|
610
|
+
org_id?: string;
|
|
611
|
+
search?: string;
|
|
612
|
+
source?: string;
|
|
613
|
+
limit?: number;
|
|
614
|
+
offset?: number;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function listCustomers(options: ListCustomersOptions = {}): Customer[] {
|
|
618
|
+
const db = getDatabase();
|
|
619
|
+
const conditions: string[] = [];
|
|
620
|
+
const params: unknown[] = [];
|
|
621
|
+
|
|
622
|
+
if (options.org_id) { conditions.push("org_id = ?"); params.push(options.org_id); }
|
|
623
|
+
if (options.search) {
|
|
624
|
+
conditions.push("(name LIKE ? OR email LIKE ? OR phone LIKE ? OR company LIKE ?)");
|
|
625
|
+
const q = `%${options.search}%`;
|
|
626
|
+
params.push(q, q, q, q);
|
|
627
|
+
}
|
|
628
|
+
if (options.source) { conditions.push("source = ?"); params.push(options.source); }
|
|
629
|
+
|
|
630
|
+
let sql = "SELECT * FROM customers";
|
|
631
|
+
if (conditions.length > 0) sql += " WHERE " + conditions.join(" AND ");
|
|
632
|
+
sql += " ORDER BY name";
|
|
633
|
+
|
|
634
|
+
if (options.limit) { sql += " LIMIT ?"; params.push(options.limit); }
|
|
635
|
+
if (options.offset) { sql += " OFFSET ?"; params.push(options.offset); }
|
|
636
|
+
|
|
637
|
+
const rows = db.prepare(sql).all(...params) as CustomerRow[];
|
|
638
|
+
return rows.map(rowToCustomer);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export interface UpdateCustomerInput {
|
|
642
|
+
name?: string;
|
|
643
|
+
email?: string;
|
|
644
|
+
phone?: string;
|
|
645
|
+
company?: string;
|
|
646
|
+
address?: Record<string, unknown>;
|
|
647
|
+
source?: string;
|
|
648
|
+
source_ids?: Record<string, unknown>;
|
|
649
|
+
tags?: string[];
|
|
650
|
+
lifetime_value?: number;
|
|
651
|
+
metadata?: Record<string, unknown>;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export function updateCustomer(id: string, input: UpdateCustomerInput): Customer | null {
|
|
655
|
+
const db = getDatabase();
|
|
656
|
+
const existing = getCustomer(id);
|
|
657
|
+
if (!existing) return null;
|
|
658
|
+
|
|
659
|
+
const sets: string[] = [];
|
|
660
|
+
const params: unknown[] = [];
|
|
661
|
+
|
|
662
|
+
if (input.name !== undefined) { sets.push("name = ?"); params.push(input.name); }
|
|
663
|
+
if (input.email !== undefined) { sets.push("email = ?"); params.push(input.email); }
|
|
664
|
+
if (input.phone !== undefined) { sets.push("phone = ?"); params.push(input.phone); }
|
|
665
|
+
if (input.company !== undefined) { sets.push("company = ?"); params.push(input.company); }
|
|
666
|
+
if (input.address !== undefined) { sets.push("address = ?"); params.push(JSON.stringify(input.address)); }
|
|
667
|
+
if (input.source !== undefined) { sets.push("source = ?"); params.push(input.source); }
|
|
668
|
+
if (input.source_ids !== undefined) { sets.push("source_ids = ?"); params.push(JSON.stringify(input.source_ids)); }
|
|
669
|
+
if (input.tags !== undefined) { sets.push("tags = ?"); params.push(JSON.stringify(input.tags)); }
|
|
670
|
+
if (input.lifetime_value !== undefined) { sets.push("lifetime_value = ?"); params.push(input.lifetime_value); }
|
|
671
|
+
if (input.metadata !== undefined) { sets.push("metadata = ?"); params.push(JSON.stringify(input.metadata)); }
|
|
672
|
+
|
|
673
|
+
if (sets.length === 0) return existing;
|
|
674
|
+
|
|
675
|
+
sets.push("updated_at = datetime('now')");
|
|
676
|
+
params.push(id);
|
|
677
|
+
|
|
678
|
+
db.prepare(`UPDATE customers SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
679
|
+
|
|
680
|
+
return getCustomer(id);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export function deleteCustomer(id: string): boolean {
|
|
684
|
+
const db = getDatabase();
|
|
685
|
+
const result = db.prepare("DELETE FROM customers WHERE id = ?").run(id);
|
|
686
|
+
return result.changes > 0;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export function searchCustomers(orgId: string, query: string): Customer[] {
|
|
690
|
+
return listCustomers({ org_id: orgId, search: query });
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export function getCustomerByEmail(orgId: string, email: string): Customer | null {
|
|
694
|
+
const db = getDatabase();
|
|
695
|
+
const row = db.prepare("SELECT * FROM customers WHERE org_id = ? AND email = ?").get(orgId, email) as CustomerRow | null;
|
|
696
|
+
return row ? rowToCustomer(row) : null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function mergeCustomers(id1: string, id2: string): Customer | null {
|
|
700
|
+
const db = getDatabase();
|
|
701
|
+
const primary = getCustomer(id1);
|
|
702
|
+
const secondary = getCustomer(id2);
|
|
703
|
+
if (!primary || !secondary) return null;
|
|
704
|
+
|
|
705
|
+
// Merge: keep primary, fill blanks from secondary, combine tags and lifetime_value
|
|
706
|
+
const mergedTags = [...new Set([...primary.tags, ...secondary.tags])];
|
|
707
|
+
const mergedLifetimeValue = primary.lifetime_value + secondary.lifetime_value;
|
|
708
|
+
const mergedSourceIds = { ...secondary.source_ids, ...primary.source_ids };
|
|
709
|
+
const mergedMetadata = { ...secondary.metadata, ...primary.metadata, merged_from: id2 };
|
|
710
|
+
|
|
711
|
+
updateCustomer(id1, {
|
|
712
|
+
email: primary.email || secondary.email || undefined,
|
|
713
|
+
phone: primary.phone || secondary.phone || undefined,
|
|
714
|
+
company: primary.company || secondary.company || undefined,
|
|
715
|
+
address: Object.keys(primary.address).length > 0 ? primary.address : secondary.address,
|
|
716
|
+
source: primary.source || secondary.source || undefined,
|
|
717
|
+
source_ids: mergedSourceIds,
|
|
718
|
+
tags: mergedTags,
|
|
719
|
+
lifetime_value: mergedLifetimeValue,
|
|
720
|
+
metadata: mergedMetadata,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// Delete the secondary customer
|
|
724
|
+
deleteCustomer(id2);
|
|
725
|
+
|
|
726
|
+
return getCustomer(id1);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// ─── Vendors ─────────────────────────────────────────────────────────────────
|
|
730
|
+
|
|
731
|
+
export interface CreateVendorInput {
|
|
732
|
+
org_id: string;
|
|
733
|
+
name: string;
|
|
734
|
+
email?: string;
|
|
735
|
+
phone?: string;
|
|
736
|
+
company?: string;
|
|
737
|
+
category?: "supplier" | "contractor" | "partner" | "agency";
|
|
738
|
+
payment_terms?: string;
|
|
739
|
+
address?: Record<string, unknown>;
|
|
740
|
+
metadata?: Record<string, unknown>;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export function createVendor(input: CreateVendorInput): Vendor {
|
|
744
|
+
const db = getDatabase();
|
|
745
|
+
const id = crypto.randomUUID();
|
|
746
|
+
|
|
747
|
+
db.prepare(
|
|
748
|
+
`INSERT INTO vendors (id, org_id, name, email, phone, company, category, payment_terms, address, metadata)
|
|
749
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
750
|
+
).run(
|
|
751
|
+
id,
|
|
752
|
+
input.org_id,
|
|
753
|
+
input.name,
|
|
754
|
+
input.email || null,
|
|
755
|
+
input.phone || null,
|
|
756
|
+
input.company || null,
|
|
757
|
+
input.category || null,
|
|
758
|
+
input.payment_terms || null,
|
|
759
|
+
JSON.stringify(input.address || {}),
|
|
760
|
+
JSON.stringify(input.metadata || {})
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
return getVendor(id)!;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export function getVendor(id: string): Vendor | null {
|
|
767
|
+
const db = getDatabase();
|
|
768
|
+
const row = db.prepare("SELECT * FROM vendors WHERE id = ?").get(id) as VendorRow | null;
|
|
769
|
+
return row ? rowToVendor(row) : null;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export interface ListVendorsOptions {
|
|
773
|
+
org_id?: string;
|
|
774
|
+
category?: string;
|
|
775
|
+
search?: string;
|
|
776
|
+
limit?: number;
|
|
777
|
+
offset?: number;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export function listVendors(options: ListVendorsOptions = {}): Vendor[] {
|
|
781
|
+
const db = getDatabase();
|
|
782
|
+
const conditions: string[] = [];
|
|
783
|
+
const params: unknown[] = [];
|
|
784
|
+
|
|
785
|
+
if (options.org_id) { conditions.push("org_id = ?"); params.push(options.org_id); }
|
|
786
|
+
if (options.category) { conditions.push("category = ?"); params.push(options.category); }
|
|
787
|
+
if (options.search) {
|
|
788
|
+
conditions.push("(name LIKE ? OR email LIKE ? OR phone LIKE ? OR company LIKE ?)");
|
|
789
|
+
const q = `%${options.search}%`;
|
|
790
|
+
params.push(q, q, q, q);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
let sql = "SELECT * FROM vendors";
|
|
794
|
+
if (conditions.length > 0) sql += " WHERE " + conditions.join(" AND ");
|
|
795
|
+
sql += " ORDER BY name";
|
|
796
|
+
|
|
797
|
+
if (options.limit) { sql += " LIMIT ?"; params.push(options.limit); }
|
|
798
|
+
if (options.offset) { sql += " OFFSET ?"; params.push(options.offset); }
|
|
799
|
+
|
|
800
|
+
const rows = db.prepare(sql).all(...params) as VendorRow[];
|
|
801
|
+
return rows.map(rowToVendor);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export interface UpdateVendorInput {
|
|
805
|
+
name?: string;
|
|
806
|
+
email?: string;
|
|
807
|
+
phone?: string;
|
|
808
|
+
company?: string;
|
|
809
|
+
category?: "supplier" | "contractor" | "partner" | "agency";
|
|
810
|
+
payment_terms?: string;
|
|
811
|
+
address?: Record<string, unknown>;
|
|
812
|
+
metadata?: Record<string, unknown>;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export function updateVendor(id: string, input: UpdateVendorInput): Vendor | null {
|
|
816
|
+
const db = getDatabase();
|
|
817
|
+
const existing = getVendor(id);
|
|
818
|
+
if (!existing) return null;
|
|
819
|
+
|
|
820
|
+
const sets: string[] = [];
|
|
821
|
+
const params: unknown[] = [];
|
|
822
|
+
|
|
823
|
+
if (input.name !== undefined) { sets.push("name = ?"); params.push(input.name); }
|
|
824
|
+
if (input.email !== undefined) { sets.push("email = ?"); params.push(input.email); }
|
|
825
|
+
if (input.phone !== undefined) { sets.push("phone = ?"); params.push(input.phone); }
|
|
826
|
+
if (input.company !== undefined) { sets.push("company = ?"); params.push(input.company); }
|
|
827
|
+
if (input.category !== undefined) { sets.push("category = ?"); params.push(input.category); }
|
|
828
|
+
if (input.payment_terms !== undefined) { sets.push("payment_terms = ?"); params.push(input.payment_terms); }
|
|
829
|
+
if (input.address !== undefined) { sets.push("address = ?"); params.push(JSON.stringify(input.address)); }
|
|
830
|
+
if (input.metadata !== undefined) { sets.push("metadata = ?"); params.push(JSON.stringify(input.metadata)); }
|
|
831
|
+
|
|
832
|
+
if (sets.length === 0) return existing;
|
|
833
|
+
|
|
834
|
+
sets.push("updated_at = datetime('now')");
|
|
835
|
+
params.push(id);
|
|
836
|
+
|
|
837
|
+
db.prepare(`UPDATE vendors SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
838
|
+
|
|
839
|
+
return getVendor(id);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export function deleteVendor(id: string): boolean {
|
|
843
|
+
const db = getDatabase();
|
|
844
|
+
const result = db.prepare("DELETE FROM vendors WHERE id = ?").run(id);
|
|
845
|
+
return result.changes > 0;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
export function searchVendors(orgId: string, query: string): Vendor[] {
|
|
849
|
+
return listVendors({ org_id: orgId, search: query });
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function getVendorsByCategory(orgId: string, category: string): Vendor[] {
|
|
853
|
+
return listVendors({ org_id: orgId, category });
|
|
854
|
+
}
|