@hasna/microservices 0.0.5 → 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-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-domains/src/cli/index.ts +420 -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 +245 -0
- package/package.json +1 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Financial consolidation — periods, P&L, cashflow, budgets
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDatabase } from "../db/database.js";
|
|
6
|
+
|
|
7
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export interface FinancialPeriod {
|
|
10
|
+
id: string;
|
|
11
|
+
org_id: string | null;
|
|
12
|
+
name: string;
|
|
13
|
+
type: "month" | "quarter" | "year";
|
|
14
|
+
start_date: string;
|
|
15
|
+
end_date: string;
|
|
16
|
+
status: "open" | "closing" | "closed";
|
|
17
|
+
revenue: number;
|
|
18
|
+
expenses: number;
|
|
19
|
+
net_income: number;
|
|
20
|
+
breakdown: Record<string, unknown>;
|
|
21
|
+
created_at: string;
|
|
22
|
+
closed_at: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PeriodRow {
|
|
26
|
+
id: string;
|
|
27
|
+
org_id: string | null;
|
|
28
|
+
name: string;
|
|
29
|
+
type: string;
|
|
30
|
+
start_date: string;
|
|
31
|
+
end_date: string;
|
|
32
|
+
status: string;
|
|
33
|
+
revenue: number;
|
|
34
|
+
expenses: number;
|
|
35
|
+
net_income: number;
|
|
36
|
+
breakdown: string;
|
|
37
|
+
created_at: string;
|
|
38
|
+
closed_at: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Budget {
|
|
42
|
+
id: string;
|
|
43
|
+
org_id: string | null;
|
|
44
|
+
department: string;
|
|
45
|
+
monthly_amount: number;
|
|
46
|
+
currency: string;
|
|
47
|
+
created_at: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PnlReport {
|
|
51
|
+
revenue: number;
|
|
52
|
+
expenses: number;
|
|
53
|
+
net_income: number;
|
|
54
|
+
breakdown_by_service: Record<string, { revenue: number; expenses: number }>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CashflowReport {
|
|
58
|
+
cash_in: number;
|
|
59
|
+
cash_out: number;
|
|
60
|
+
net_cashflow: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface BudgetVsActual {
|
|
64
|
+
department: string;
|
|
65
|
+
budget: number;
|
|
66
|
+
actual: number;
|
|
67
|
+
variance: number;
|
|
68
|
+
variance_pct: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Row converters ──────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
function rowToPeriod(row: PeriodRow): FinancialPeriod {
|
|
74
|
+
return {
|
|
75
|
+
...row,
|
|
76
|
+
type: row.type as FinancialPeriod["type"],
|
|
77
|
+
status: row.status as FinancialPeriod["status"],
|
|
78
|
+
breakdown: JSON.parse(row.breakdown || "{}"),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Financial Periods ───────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export function createPeriod(
|
|
85
|
+
orgId: string,
|
|
86
|
+
name: string,
|
|
87
|
+
type: "month" | "quarter" | "year",
|
|
88
|
+
startDate: string,
|
|
89
|
+
endDate: string
|
|
90
|
+
): FinancialPeriod {
|
|
91
|
+
const db = getDatabase();
|
|
92
|
+
const id = crypto.randomUUID();
|
|
93
|
+
|
|
94
|
+
db.prepare(
|
|
95
|
+
`INSERT INTO financial_periods (id, org_id, name, type, start_date, end_date)
|
|
96
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
97
|
+
).run(id, orgId, name, type, startDate, endDate);
|
|
98
|
+
|
|
99
|
+
return getPeriod(id)!;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getPeriod(id: string): FinancialPeriod | null {
|
|
103
|
+
const db = getDatabase();
|
|
104
|
+
const row = db.prepare("SELECT * FROM financial_periods WHERE id = ?").get(id) as PeriodRow | null;
|
|
105
|
+
return row ? rowToPeriod(row) : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function listPeriods(orgId: string, type?: string): FinancialPeriod[] {
|
|
109
|
+
const db = getDatabase();
|
|
110
|
+
const conditions: string[] = ["org_id = ?"];
|
|
111
|
+
const params: unknown[] = [orgId];
|
|
112
|
+
|
|
113
|
+
if (type) {
|
|
114
|
+
conditions.push("type = ?");
|
|
115
|
+
params.push(type);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const sql = `SELECT * FROM financial_periods WHERE ${conditions.join(" AND ")} ORDER BY start_date DESC`;
|
|
119
|
+
const rows = db.prepare(sql).all(...params) as PeriodRow[];
|
|
120
|
+
return rows.map(rowToPeriod);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function closePeriod(
|
|
124
|
+
periodId: string,
|
|
125
|
+
revenue: number,
|
|
126
|
+
expenses: number
|
|
127
|
+
): FinancialPeriod | null {
|
|
128
|
+
const db = getDatabase();
|
|
129
|
+
const existing = getPeriod(periodId);
|
|
130
|
+
if (!existing) return null;
|
|
131
|
+
|
|
132
|
+
const netIncome = revenue - expenses;
|
|
133
|
+
const breakdown = {
|
|
134
|
+
revenue_snapshot: revenue,
|
|
135
|
+
expenses_snapshot: expenses,
|
|
136
|
+
closed_by: "system",
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
db.prepare(
|
|
140
|
+
`UPDATE financial_periods
|
|
141
|
+
SET status = 'closed',
|
|
142
|
+
revenue = ?,
|
|
143
|
+
expenses = ?,
|
|
144
|
+
net_income = ?,
|
|
145
|
+
breakdown = ?,
|
|
146
|
+
closed_at = datetime('now')
|
|
147
|
+
WHERE id = ?`
|
|
148
|
+
).run(revenue, expenses, netIncome, JSON.stringify(breakdown), periodId);
|
|
149
|
+
|
|
150
|
+
return getPeriod(periodId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── P&L Report ──────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
export function generatePnl(orgId: string, startDate: string, endDate: string): PnlReport {
|
|
156
|
+
const db = getDatabase();
|
|
157
|
+
|
|
158
|
+
// Aggregate from closed financial periods that overlap the date range
|
|
159
|
+
const rows = db.prepare(
|
|
160
|
+
`SELECT name, revenue, expenses, breakdown
|
|
161
|
+
FROM financial_periods
|
|
162
|
+
WHERE org_id = ?
|
|
163
|
+
AND start_date >= ?
|
|
164
|
+
AND end_date <= ?
|
|
165
|
+
AND status = 'closed'
|
|
166
|
+
ORDER BY start_date`
|
|
167
|
+
).all(orgId, startDate, endDate) as PeriodRow[];
|
|
168
|
+
|
|
169
|
+
let totalRevenue = 0;
|
|
170
|
+
let totalExpenses = 0;
|
|
171
|
+
const breakdownByService: Record<string, { revenue: number; expenses: number }> = {};
|
|
172
|
+
|
|
173
|
+
for (const row of rows) {
|
|
174
|
+
totalRevenue += row.revenue;
|
|
175
|
+
totalExpenses += row.expenses;
|
|
176
|
+
|
|
177
|
+
// Use the period name as the "service" key for breakdown
|
|
178
|
+
breakdownByService[row.name] = {
|
|
179
|
+
revenue: row.revenue,
|
|
180
|
+
expenses: row.expenses,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
revenue: totalRevenue,
|
|
186
|
+
expenses: totalExpenses,
|
|
187
|
+
net_income: totalRevenue - totalExpenses,
|
|
188
|
+
breakdown_by_service: breakdownByService,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Cashflow Report ─────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
export function generateCashflow(orgId: string, startDate: string, endDate: string): CashflowReport {
|
|
195
|
+
const db = getDatabase();
|
|
196
|
+
|
|
197
|
+
// Aggregate from financial periods (all statuses) that overlap the date range
|
|
198
|
+
const row = db.prepare(
|
|
199
|
+
`SELECT COALESCE(SUM(revenue), 0) as cash_in,
|
|
200
|
+
COALESCE(SUM(expenses), 0) as cash_out
|
|
201
|
+
FROM financial_periods
|
|
202
|
+
WHERE org_id = ?
|
|
203
|
+
AND start_date >= ?
|
|
204
|
+
AND end_date <= ?`
|
|
205
|
+
).get(orgId, startDate, endDate) as { cash_in: number; cash_out: number } | null;
|
|
206
|
+
|
|
207
|
+
const cashIn = row?.cash_in ?? 0;
|
|
208
|
+
const cashOut = row?.cash_out ?? 0;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
cash_in: cashIn,
|
|
212
|
+
cash_out: cashOut,
|
|
213
|
+
net_cashflow: cashIn - cashOut,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Budgets ─────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
export function setBudget(orgId: string, department: string, monthlyAmount: number): Budget {
|
|
220
|
+
const db = getDatabase();
|
|
221
|
+
|
|
222
|
+
// Upsert: if budget exists for this org+department, update it; otherwise create
|
|
223
|
+
const existing = db.prepare(
|
|
224
|
+
"SELECT id FROM budgets WHERE org_id = ? AND department = ?"
|
|
225
|
+
).get(orgId, department) as { id: string } | null;
|
|
226
|
+
|
|
227
|
+
if (existing) {
|
|
228
|
+
db.prepare("UPDATE budgets SET monthly_amount = ? WHERE id = ?").run(monthlyAmount, existing.id);
|
|
229
|
+
return getBudget(existing.id)!;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const id = crypto.randomUUID();
|
|
233
|
+
db.prepare(
|
|
234
|
+
`INSERT INTO budgets (id, org_id, department, monthly_amount)
|
|
235
|
+
VALUES (?, ?, ?, ?)`
|
|
236
|
+
).run(id, orgId, department, monthlyAmount);
|
|
237
|
+
|
|
238
|
+
return getBudget(id)!;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function getBudget(id: string): Budget | null {
|
|
242
|
+
const db = getDatabase();
|
|
243
|
+
const row = db.prepare("SELECT * FROM budgets WHERE id = ?").get(id) as Budget | null;
|
|
244
|
+
return row ?? null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function listBudgets(orgId: string): Budget[] {
|
|
248
|
+
const db = getDatabase();
|
|
249
|
+
return db.prepare(
|
|
250
|
+
"SELECT * FROM budgets WHERE org_id = ? ORDER BY department"
|
|
251
|
+
).all(orgId) as Budget[];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function getBudgetVsActual(
|
|
255
|
+
orgId: string,
|
|
256
|
+
department: string,
|
|
257
|
+
month: string
|
|
258
|
+
): BudgetVsActual | null {
|
|
259
|
+
const db = getDatabase();
|
|
260
|
+
|
|
261
|
+
// Get budget for this department
|
|
262
|
+
const budget = db.prepare(
|
|
263
|
+
"SELECT monthly_amount FROM budgets WHERE org_id = ? AND department = ?"
|
|
264
|
+
).get(orgId, department) as { monthly_amount: number } | null;
|
|
265
|
+
|
|
266
|
+
if (!budget) return null;
|
|
267
|
+
|
|
268
|
+
// Calculate month boundaries (month is "YYYY-MM")
|
|
269
|
+
const startDate = `${month}-01`;
|
|
270
|
+
const [yearStr, monthStr] = month.split("-");
|
|
271
|
+
const year = parseInt(yearStr, 10);
|
|
272
|
+
const mon = parseInt(monthStr, 10);
|
|
273
|
+
const lastDay = new Date(year, mon, 0).getDate();
|
|
274
|
+
const endDate = `${month}-${String(lastDay).padStart(2, "0")}`;
|
|
275
|
+
|
|
276
|
+
// Sum expenses from financial periods that match this department name and date range
|
|
277
|
+
const row = db.prepare(
|
|
278
|
+
`SELECT COALESCE(SUM(expenses), 0) as actual
|
|
279
|
+
FROM financial_periods
|
|
280
|
+
WHERE org_id = ?
|
|
281
|
+
AND start_date >= ?
|
|
282
|
+
AND end_date <= ?
|
|
283
|
+
AND name LIKE ?`
|
|
284
|
+
).get(orgId, startDate, endDate, `%${department}%`) as { actual: number } | null;
|
|
285
|
+
|
|
286
|
+
const actual = row?.actual ?? 0;
|
|
287
|
+
const variance = budget.monthly_amount - actual;
|
|
288
|
+
const variancePct = budget.monthly_amount > 0
|
|
289
|
+
? ((variance / budget.monthly_amount) * 100)
|
|
290
|
+
: 0;
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
department,
|
|
294
|
+
budget: budget.monthly_amount,
|
|
295
|
+
actual,
|
|
296
|
+
variance,
|
|
297
|
+
variance_pct: Math.round(variancePct * 100) / 100,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company settings — key/value configuration per organization
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDatabase } from "../db/database.js";
|
|
6
|
+
|
|
7
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export interface CompanySetting {
|
|
10
|
+
id: string;
|
|
11
|
+
org_id: string | null;
|
|
12
|
+
key: string;
|
|
13
|
+
value: string;
|
|
14
|
+
category: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─── Operations ──────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export function getSetting(orgId: string | null, key: string): CompanySetting | null {
|
|
20
|
+
const db = getDatabase();
|
|
21
|
+
const row = db.prepare(
|
|
22
|
+
"SELECT * FROM company_settings WHERE org_id IS ? AND key = ?"
|
|
23
|
+
).get(orgId, key) as CompanySetting | null;
|
|
24
|
+
return row;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function setSetting(
|
|
28
|
+
orgId: string | null,
|
|
29
|
+
key: string,
|
|
30
|
+
value: string,
|
|
31
|
+
category?: string
|
|
32
|
+
): CompanySetting {
|
|
33
|
+
const db = getDatabase();
|
|
34
|
+
const existing = getSetting(orgId, key);
|
|
35
|
+
|
|
36
|
+
if (existing) {
|
|
37
|
+
db.prepare(
|
|
38
|
+
"UPDATE company_settings SET value = ?, category = COALESCE(?, category) WHERE id = ?"
|
|
39
|
+
).run(value, category || null, existing.id);
|
|
40
|
+
return getSetting(orgId, key)!;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const id = crypto.randomUUID();
|
|
44
|
+
db.prepare(
|
|
45
|
+
"INSERT INTO company_settings (id, org_id, key, value, category) VALUES (?, ?, ?, ?, ?)"
|
|
46
|
+
).run(id, orgId, key, value, category || null);
|
|
47
|
+
|
|
48
|
+
return getSetting(orgId, key)!;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getAllSettings(orgId: string | null, category?: string): CompanySetting[] {
|
|
52
|
+
const db = getDatabase();
|
|
53
|
+
const conditions: string[] = ["org_id IS ?"];
|
|
54
|
+
const params: unknown[] = [orgId];
|
|
55
|
+
|
|
56
|
+
if (category) {
|
|
57
|
+
conditions.push("category = ?");
|
|
58
|
+
params.push(category);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const sql = `SELECT * FROM company_settings WHERE ${conditions.join(" AND ")} ORDER BY key`;
|
|
62
|
+
return db.prepare(sql).all(...params) as CompanySetting[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function deleteSetting(orgId: string | null, key: string): boolean {
|
|
66
|
+
const db = getDatabase();
|
|
67
|
+
const result = db.prepare(
|
|
68
|
+
"DELETE FROM company_settings WHERE org_id IS ? AND key = ?"
|
|
69
|
+
).run(orgId, key);
|
|
70
|
+
return result.changes > 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface BulkSettingEntry {
|
|
74
|
+
key: string;
|
|
75
|
+
value: string;
|
|
76
|
+
category?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function bulkSetSettings(orgId: string | null, entries: BulkSettingEntry[]): CompanySetting[] {
|
|
80
|
+
const results: CompanySetting[] = [];
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
results.push(setSetting(orgId, entry.key, entry.value, entry.category));
|
|
83
|
+
}
|
|
84
|
+
return results;
|
|
85
|
+
}
|