@hasna/microservices 0.0.2 → 0.0.4
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 +70 -0
- package/bin/mcp.js +71 -1
- package/dist/index.js +70 -0
- package/microservices/microservice-ads/package.json +27 -0
- package/microservices/microservice-ads/src/cli/index.ts +407 -0
- package/microservices/microservice-ads/src/db/campaigns.ts +493 -0
- package/microservices/microservice-ads/src/db/database.ts +93 -0
- package/microservices/microservice-ads/src/db/migrations.ts +60 -0
- package/microservices/microservice-ads/src/index.ts +39 -0
- package/microservices/microservice-ads/src/mcp/index.ts +320 -0
- package/microservices/microservice-contracts/package.json +27 -0
- package/microservices/microservice-contracts/src/cli/index.ts +383 -0
- package/microservices/microservice-contracts/src/db/contracts.ts +496 -0
- package/microservices/microservice-contracts/src/db/database.ts +93 -0
- package/microservices/microservice-contracts/src/db/migrations.ts +58 -0
- package/microservices/microservice-contracts/src/index.ts +43 -0
- package/microservices/microservice-contracts/src/mcp/index.ts +308 -0
- package/microservices/microservice-domains/package.json +27 -0
- package/microservices/microservice-domains/src/cli/index.ts +438 -0
- package/microservices/microservice-domains/src/db/database.ts +93 -0
- package/microservices/microservice-domains/src/db/domains.ts +551 -0
- package/microservices/microservice-domains/src/db/migrations.ts +60 -0
- package/microservices/microservice-domains/src/index.ts +44 -0
- package/microservices/microservice-domains/src/mcp/index.ts +368 -0
- package/microservices/microservice-hiring/package.json +27 -0
- package/microservices/microservice-hiring/src/cli/index.ts +431 -0
- package/microservices/microservice-hiring/src/db/database.ts +93 -0
- package/microservices/microservice-hiring/src/db/hiring.ts +582 -0
- package/microservices/microservice-hiring/src/db/migrations.ts +68 -0
- package/microservices/microservice-hiring/src/index.ts +51 -0
- package/microservices/microservice-hiring/src/mcp/index.ts +464 -0
- package/microservices/microservice-payments/package.json +27 -0
- package/microservices/microservice-payments/src/cli/index.ts +357 -0
- package/microservices/microservice-payments/src/db/database.ts +93 -0
- package/microservices/microservice-payments/src/db/migrations.ts +63 -0
- package/microservices/microservice-payments/src/db/payments.ts +652 -0
- package/microservices/microservice-payments/src/index.ts +51 -0
- package/microservices/microservice-payments/src/mcp/index.ts +460 -0
- package/microservices/microservice-payroll/package.json +27 -0
- package/microservices/microservice-payroll/src/cli/index.ts +374 -0
- package/microservices/microservice-payroll/src/db/database.ts +93 -0
- package/microservices/microservice-payroll/src/db/migrations.ts +69 -0
- package/microservices/microservice-payroll/src/db/payroll.ts +741 -0
- package/microservices/microservice-payroll/src/index.ts +48 -0
- package/microservices/microservice-payroll/src/mcp/index.ts +420 -0
- package/microservices/microservice-shipping/package.json +27 -0
- package/microservices/microservice-shipping/src/cli/index.ts +398 -0
- package/microservices/microservice-shipping/src/db/database.ts +93 -0
- package/microservices/microservice-shipping/src/db/migrations.ts +61 -0
- package/microservices/microservice-shipping/src/db/shipping.ts +643 -0
- package/microservices/microservice-shipping/src/index.ts +53 -0
- package/microservices/microservice-shipping/src/mcp/index.ts +385 -0
- package/microservices/microservice-social/package.json +27 -0
- package/microservices/microservice-social/src/cli/index.ts +447 -0
- package/microservices/microservice-social/src/db/database.ts +93 -0
- package/microservices/microservice-social/src/db/migrations.ts +55 -0
- package/microservices/microservice-social/src/db/social.ts +672 -0
- package/microservices/microservice-social/src/index.ts +46 -0
- package/microservices/microservice-social/src/mcp/index.ts +435 -0
- package/microservices/microservice-subscriptions/package.json +27 -0
- package/microservices/microservice-subscriptions/src/cli/index.ts +400 -0
- package/microservices/microservice-subscriptions/src/db/database.ts +93 -0
- package/microservices/microservice-subscriptions/src/db/migrations.ts +57 -0
- package/microservices/microservice-subscriptions/src/db/subscriptions.ts +692 -0
- package/microservices/microservice-subscriptions/src/index.ts +41 -0
- package/microservices/microservice-subscriptions/src/mcp/index.ts +365 -0
- package/microservices/microservice-transcriber/package.json +28 -0
- package/microservices/microservice-transcriber/src/cli/index.ts +1347 -0
- package/microservices/microservice-transcriber/src/db/annotations.ts +37 -0
- package/microservices/microservice-transcriber/src/db/database.ts +82 -0
- package/microservices/microservice-transcriber/src/db/migrations.ts +72 -0
- package/microservices/microservice-transcriber/src/db/transcripts.ts +395 -0
- package/microservices/microservice-transcriber/src/index.ts +43 -0
- package/microservices/microservice-transcriber/src/lib/config.ts +77 -0
- package/microservices/microservice-transcriber/src/lib/diff.ts +91 -0
- package/microservices/microservice-transcriber/src/lib/downloader.ts +570 -0
- package/microservices/microservice-transcriber/src/lib/feeds.ts +62 -0
- package/microservices/microservice-transcriber/src/lib/live.ts +94 -0
- package/microservices/microservice-transcriber/src/lib/notion.ts +129 -0
- package/microservices/microservice-transcriber/src/lib/providers.ts +713 -0
- package/microservices/microservice-transcriber/src/lib/summarizer.ts +147 -0
- package/microservices/microservice-transcriber/src/lib/translator.ts +75 -0
- package/microservices/microservice-transcriber/src/lib/webhook.ts +37 -0
- package/microservices/microservice-transcriber/src/mcp/index.ts +1070 -0
- package/microservices/microservice-transcriber/src/server/index.ts +199 -0
- package/package.json +1 -1
- package/microservices/microservice-invoices/dashboard/dist/assets/index-Bngq7FNM.css +0 -1
- package/microservices/microservice-invoices/dashboard/dist/assets/index-aHW4ARZR.js +0 -124
- package/microservices/microservice-invoices/dashboard/dist/index.html +0 -13
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payroll CRUD and business logic operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDatabase } from "./database.js";
|
|
6
|
+
|
|
7
|
+
// --- Types ---
|
|
8
|
+
|
|
9
|
+
export interface Employee {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
email: string | null;
|
|
13
|
+
type: "employee" | "contractor";
|
|
14
|
+
status: "active" | "terminated";
|
|
15
|
+
department: string | null;
|
|
16
|
+
title: string | null;
|
|
17
|
+
pay_rate: number;
|
|
18
|
+
pay_type: "salary" | "hourly";
|
|
19
|
+
currency: string;
|
|
20
|
+
tax_info: Record<string, unknown>;
|
|
21
|
+
start_date: string | null;
|
|
22
|
+
end_date: string | null;
|
|
23
|
+
created_at: string;
|
|
24
|
+
updated_at: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface EmployeeRow {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
email: string | null;
|
|
31
|
+
type: string;
|
|
32
|
+
status: string;
|
|
33
|
+
department: string | null;
|
|
34
|
+
title: string | null;
|
|
35
|
+
pay_rate: number;
|
|
36
|
+
pay_type: string;
|
|
37
|
+
currency: string;
|
|
38
|
+
tax_info: string;
|
|
39
|
+
start_date: string | null;
|
|
40
|
+
end_date: string | null;
|
|
41
|
+
created_at: string;
|
|
42
|
+
updated_at: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rowToEmployee(row: EmployeeRow): Employee {
|
|
46
|
+
return {
|
|
47
|
+
...row,
|
|
48
|
+
type: row.type as Employee["type"],
|
|
49
|
+
status: row.status as Employee["status"],
|
|
50
|
+
pay_type: row.pay_type as Employee["pay_type"],
|
|
51
|
+
tax_info: JSON.parse(row.tax_info || "{}"),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface PayPeriod {
|
|
56
|
+
id: string;
|
|
57
|
+
start_date: string;
|
|
58
|
+
end_date: string;
|
|
59
|
+
status: "draft" | "processing" | "completed";
|
|
60
|
+
created_at: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface PayStub {
|
|
64
|
+
id: string;
|
|
65
|
+
employee_id: string;
|
|
66
|
+
pay_period_id: string;
|
|
67
|
+
gross_pay: number;
|
|
68
|
+
deductions: Record<string, number>;
|
|
69
|
+
net_pay: number;
|
|
70
|
+
hours_worked: number | null;
|
|
71
|
+
overtime_hours: number;
|
|
72
|
+
created_at: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface PayStubRow {
|
|
76
|
+
id: string;
|
|
77
|
+
employee_id: string;
|
|
78
|
+
pay_period_id: string;
|
|
79
|
+
gross_pay: number;
|
|
80
|
+
deductions: string;
|
|
81
|
+
net_pay: number;
|
|
82
|
+
hours_worked: number | null;
|
|
83
|
+
overtime_hours: number;
|
|
84
|
+
created_at: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function rowToPayStub(row: PayStubRow): PayStub {
|
|
88
|
+
return {
|
|
89
|
+
...row,
|
|
90
|
+
deductions: JSON.parse(row.deductions || "{}"),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface Payment {
|
|
95
|
+
id: string;
|
|
96
|
+
pay_stub_id: string;
|
|
97
|
+
method: "direct_deposit" | "check" | "wire";
|
|
98
|
+
status: "pending" | "paid" | "failed";
|
|
99
|
+
paid_at: string | null;
|
|
100
|
+
reference: string | null;
|
|
101
|
+
created_at: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- Employee CRUD ---
|
|
105
|
+
|
|
106
|
+
export interface CreateEmployeeInput {
|
|
107
|
+
name: string;
|
|
108
|
+
email?: string;
|
|
109
|
+
type?: "employee" | "contractor";
|
|
110
|
+
status?: "active" | "terminated";
|
|
111
|
+
department?: string;
|
|
112
|
+
title?: string;
|
|
113
|
+
pay_rate: number;
|
|
114
|
+
pay_type?: "salary" | "hourly";
|
|
115
|
+
currency?: string;
|
|
116
|
+
tax_info?: Record<string, unknown>;
|
|
117
|
+
start_date?: string;
|
|
118
|
+
end_date?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function createEmployee(input: CreateEmployeeInput): Employee {
|
|
122
|
+
const db = getDatabase();
|
|
123
|
+
const id = crypto.randomUUID();
|
|
124
|
+
const taxInfo = JSON.stringify(input.tax_info || {});
|
|
125
|
+
|
|
126
|
+
db.prepare(
|
|
127
|
+
`INSERT INTO employees (id, name, email, type, status, department, title, pay_rate, pay_type, currency, tax_info, start_date, end_date)
|
|
128
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
129
|
+
).run(
|
|
130
|
+
id,
|
|
131
|
+
input.name,
|
|
132
|
+
input.email || null,
|
|
133
|
+
input.type || "employee",
|
|
134
|
+
input.status || "active",
|
|
135
|
+
input.department || null,
|
|
136
|
+
input.title || null,
|
|
137
|
+
input.pay_rate,
|
|
138
|
+
input.pay_type || "salary",
|
|
139
|
+
input.currency || "USD",
|
|
140
|
+
taxInfo,
|
|
141
|
+
input.start_date || null,
|
|
142
|
+
input.end_date || null
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return getEmployee(id)!;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getEmployee(id: string): Employee | null {
|
|
149
|
+
const db = getDatabase();
|
|
150
|
+
const row = db.prepare("SELECT * FROM employees WHERE id = ?").get(id) as EmployeeRow | null;
|
|
151
|
+
return row ? rowToEmployee(row) : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface ListEmployeesOptions {
|
|
155
|
+
search?: string;
|
|
156
|
+
status?: string;
|
|
157
|
+
department?: string;
|
|
158
|
+
type?: string;
|
|
159
|
+
limit?: number;
|
|
160
|
+
offset?: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function listEmployees(options: ListEmployeesOptions = {}): Employee[] {
|
|
164
|
+
const db = getDatabase();
|
|
165
|
+
const conditions: string[] = [];
|
|
166
|
+
const params: unknown[] = [];
|
|
167
|
+
|
|
168
|
+
if (options.search) {
|
|
169
|
+
conditions.push("(name LIKE ? OR email LIKE ? OR department LIKE ?)");
|
|
170
|
+
const q = `%${options.search}%`;
|
|
171
|
+
params.push(q, q, q);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (options.status) {
|
|
175
|
+
conditions.push("status = ?");
|
|
176
|
+
params.push(options.status);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (options.department) {
|
|
180
|
+
conditions.push("department = ?");
|
|
181
|
+
params.push(options.department);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (options.type) {
|
|
185
|
+
conditions.push("type = ?");
|
|
186
|
+
params.push(options.type);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let sql = "SELECT * FROM employees";
|
|
190
|
+
if (conditions.length > 0) {
|
|
191
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
192
|
+
}
|
|
193
|
+
sql += " ORDER BY name";
|
|
194
|
+
|
|
195
|
+
if (options.limit) {
|
|
196
|
+
sql += " LIMIT ?";
|
|
197
|
+
params.push(options.limit);
|
|
198
|
+
}
|
|
199
|
+
if (options.offset) {
|
|
200
|
+
sql += " OFFSET ?";
|
|
201
|
+
params.push(options.offset);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const rows = db.prepare(sql).all(...params) as EmployeeRow[];
|
|
205
|
+
return rows.map(rowToEmployee);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface UpdateEmployeeInput {
|
|
209
|
+
name?: string;
|
|
210
|
+
email?: string;
|
|
211
|
+
type?: "employee" | "contractor";
|
|
212
|
+
status?: "active" | "terminated";
|
|
213
|
+
department?: string;
|
|
214
|
+
title?: string;
|
|
215
|
+
pay_rate?: number;
|
|
216
|
+
pay_type?: "salary" | "hourly";
|
|
217
|
+
currency?: string;
|
|
218
|
+
tax_info?: Record<string, unknown>;
|
|
219
|
+
start_date?: string;
|
|
220
|
+
end_date?: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function updateEmployee(id: string, input: UpdateEmployeeInput): Employee | null {
|
|
224
|
+
const db = getDatabase();
|
|
225
|
+
const existing = getEmployee(id);
|
|
226
|
+
if (!existing) return null;
|
|
227
|
+
|
|
228
|
+
const sets: string[] = [];
|
|
229
|
+
const params: unknown[] = [];
|
|
230
|
+
|
|
231
|
+
if (input.name !== undefined) { sets.push("name = ?"); params.push(input.name); }
|
|
232
|
+
if (input.email !== undefined) { sets.push("email = ?"); params.push(input.email); }
|
|
233
|
+
if (input.type !== undefined) { sets.push("type = ?"); params.push(input.type); }
|
|
234
|
+
if (input.status !== undefined) { sets.push("status = ?"); params.push(input.status); }
|
|
235
|
+
if (input.department !== undefined) { sets.push("department = ?"); params.push(input.department); }
|
|
236
|
+
if (input.title !== undefined) { sets.push("title = ?"); params.push(input.title); }
|
|
237
|
+
if (input.pay_rate !== undefined) { sets.push("pay_rate = ?"); params.push(input.pay_rate); }
|
|
238
|
+
if (input.pay_type !== undefined) { sets.push("pay_type = ?"); params.push(input.pay_type); }
|
|
239
|
+
if (input.currency !== undefined) { sets.push("currency = ?"); params.push(input.currency); }
|
|
240
|
+
if (input.tax_info !== undefined) { sets.push("tax_info = ?"); params.push(JSON.stringify(input.tax_info)); }
|
|
241
|
+
if (input.start_date !== undefined) { sets.push("start_date = ?"); params.push(input.start_date); }
|
|
242
|
+
if (input.end_date !== undefined) { sets.push("end_date = ?"); params.push(input.end_date); }
|
|
243
|
+
|
|
244
|
+
if (sets.length === 0) return existing;
|
|
245
|
+
|
|
246
|
+
sets.push("updated_at = datetime('now')");
|
|
247
|
+
params.push(id);
|
|
248
|
+
|
|
249
|
+
db.prepare(`UPDATE employees SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
250
|
+
|
|
251
|
+
return getEmployee(id);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function deleteEmployee(id: string): boolean {
|
|
255
|
+
const db = getDatabase();
|
|
256
|
+
const result = db.prepare("DELETE FROM employees WHERE id = ?").run(id);
|
|
257
|
+
return result.changes > 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function terminateEmployee(id: string, endDate?: string): Employee | null {
|
|
261
|
+
return updateEmployee(id, {
|
|
262
|
+
status: "terminated",
|
|
263
|
+
end_date: endDate || new Date().toISOString().split("T")[0],
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function countEmployees(): number {
|
|
268
|
+
const db = getDatabase();
|
|
269
|
+
const row = db.prepare("SELECT COUNT(*) as count FROM employees").get() as { count: number };
|
|
270
|
+
return row.count;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// --- Pay Period CRUD ---
|
|
274
|
+
|
|
275
|
+
export interface CreatePayPeriodInput {
|
|
276
|
+
start_date: string;
|
|
277
|
+
end_date: string;
|
|
278
|
+
status?: "draft" | "processing" | "completed";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function createPayPeriod(input: CreatePayPeriodInput): PayPeriod {
|
|
282
|
+
const db = getDatabase();
|
|
283
|
+
const id = crypto.randomUUID();
|
|
284
|
+
|
|
285
|
+
db.prepare(
|
|
286
|
+
`INSERT INTO pay_periods (id, start_date, end_date, status) VALUES (?, ?, ?, ?)`
|
|
287
|
+
).run(id, input.start_date, input.end_date, input.status || "draft");
|
|
288
|
+
|
|
289
|
+
return getPayPeriod(id)!;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function getPayPeriod(id: string): PayPeriod | null {
|
|
293
|
+
const db = getDatabase();
|
|
294
|
+
const row = db.prepare("SELECT * FROM pay_periods WHERE id = ?").get(id) as PayPeriod | null;
|
|
295
|
+
return row || null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function listPayPeriods(status?: string): PayPeriod[] {
|
|
299
|
+
const db = getDatabase();
|
|
300
|
+
if (status) {
|
|
301
|
+
return db.prepare("SELECT * FROM pay_periods WHERE status = ? ORDER BY start_date DESC").all(status) as PayPeriod[];
|
|
302
|
+
}
|
|
303
|
+
return db.prepare("SELECT * FROM pay_periods ORDER BY start_date DESC").all() as PayPeriod[];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function updatePayPeriodStatus(id: string, status: "draft" | "processing" | "completed"): PayPeriod | null {
|
|
307
|
+
const db = getDatabase();
|
|
308
|
+
const existing = getPayPeriod(id);
|
|
309
|
+
if (!existing) return null;
|
|
310
|
+
|
|
311
|
+
db.prepare("UPDATE pay_periods SET status = ? WHERE id = ?").run(status, id);
|
|
312
|
+
return getPayPeriod(id);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function deletePayPeriod(id: string): boolean {
|
|
316
|
+
const db = getDatabase();
|
|
317
|
+
const result = db.prepare("DELETE FROM pay_periods WHERE id = ?").run(id);
|
|
318
|
+
return result.changes > 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// --- Pay Stub CRUD ---
|
|
322
|
+
|
|
323
|
+
export interface CreatePayStubInput {
|
|
324
|
+
employee_id: string;
|
|
325
|
+
pay_period_id: string;
|
|
326
|
+
gross_pay: number;
|
|
327
|
+
deductions?: Record<string, number>;
|
|
328
|
+
net_pay: number;
|
|
329
|
+
hours_worked?: number;
|
|
330
|
+
overtime_hours?: number;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function createPayStub(input: CreatePayStubInput): PayStub {
|
|
334
|
+
const db = getDatabase();
|
|
335
|
+
const id = crypto.randomUUID();
|
|
336
|
+
const deductions = JSON.stringify(input.deductions || {});
|
|
337
|
+
|
|
338
|
+
db.prepare(
|
|
339
|
+
`INSERT INTO pay_stubs (id, employee_id, pay_period_id, gross_pay, deductions, net_pay, hours_worked, overtime_hours)
|
|
340
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
341
|
+
).run(
|
|
342
|
+
id,
|
|
343
|
+
input.employee_id,
|
|
344
|
+
input.pay_period_id,
|
|
345
|
+
input.gross_pay,
|
|
346
|
+
deductions,
|
|
347
|
+
input.net_pay,
|
|
348
|
+
input.hours_worked ?? null,
|
|
349
|
+
input.overtime_hours ?? 0
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
return getPayStub(id)!;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function getPayStub(id: string): PayStub | null {
|
|
356
|
+
const db = getDatabase();
|
|
357
|
+
const row = db.prepare("SELECT * FROM pay_stubs WHERE id = ?").get(id) as PayStubRow | null;
|
|
358
|
+
return row ? rowToPayStub(row) : null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function listPayStubs(options: { employee_id?: string; pay_period_id?: string } = {}): PayStub[] {
|
|
362
|
+
const db = getDatabase();
|
|
363
|
+
const conditions: string[] = [];
|
|
364
|
+
const params: unknown[] = [];
|
|
365
|
+
|
|
366
|
+
if (options.employee_id) {
|
|
367
|
+
conditions.push("employee_id = ?");
|
|
368
|
+
params.push(options.employee_id);
|
|
369
|
+
}
|
|
370
|
+
if (options.pay_period_id) {
|
|
371
|
+
conditions.push("pay_period_id = ?");
|
|
372
|
+
params.push(options.pay_period_id);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let sql = "SELECT * FROM pay_stubs";
|
|
376
|
+
if (conditions.length > 0) {
|
|
377
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
378
|
+
}
|
|
379
|
+
sql += " ORDER BY created_at DESC";
|
|
380
|
+
|
|
381
|
+
const rows = db.prepare(sql).all(...params) as PayStubRow[];
|
|
382
|
+
return rows.map(rowToPayStub);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function deletePayStub(id: string): boolean {
|
|
386
|
+
const db = getDatabase();
|
|
387
|
+
const result = db.prepare("DELETE FROM pay_stubs WHERE id = ?").run(id);
|
|
388
|
+
return result.changes > 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// --- Payment CRUD ---
|
|
392
|
+
|
|
393
|
+
export interface CreatePaymentInput {
|
|
394
|
+
pay_stub_id: string;
|
|
395
|
+
method?: "direct_deposit" | "check" | "wire";
|
|
396
|
+
status?: "pending" | "paid" | "failed";
|
|
397
|
+
reference?: string;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function createPayment(input: CreatePaymentInput): Payment {
|
|
401
|
+
const db = getDatabase();
|
|
402
|
+
const id = crypto.randomUUID();
|
|
403
|
+
|
|
404
|
+
db.prepare(
|
|
405
|
+
`INSERT INTO payments (id, pay_stub_id, method, status, reference)
|
|
406
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
407
|
+
).run(
|
|
408
|
+
id,
|
|
409
|
+
input.pay_stub_id,
|
|
410
|
+
input.method || "direct_deposit",
|
|
411
|
+
input.status || "pending",
|
|
412
|
+
input.reference || null
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
return getPayment(id)!;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function getPayment(id: string): Payment | null {
|
|
419
|
+
const db = getDatabase();
|
|
420
|
+
const row = db.prepare("SELECT * FROM payments WHERE id = ?").get(id) as Payment | null;
|
|
421
|
+
return row || null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function listPayments(options: { pay_stub_id?: string; status?: string } = {}): Payment[] {
|
|
425
|
+
const db = getDatabase();
|
|
426
|
+
const conditions: string[] = [];
|
|
427
|
+
const params: unknown[] = [];
|
|
428
|
+
|
|
429
|
+
if (options.pay_stub_id) {
|
|
430
|
+
conditions.push("pay_stub_id = ?");
|
|
431
|
+
params.push(options.pay_stub_id);
|
|
432
|
+
}
|
|
433
|
+
if (options.status) {
|
|
434
|
+
conditions.push("status = ?");
|
|
435
|
+
params.push(options.status);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let sql = "SELECT * FROM payments";
|
|
439
|
+
if (conditions.length > 0) {
|
|
440
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
441
|
+
}
|
|
442
|
+
sql += " ORDER BY created_at DESC";
|
|
443
|
+
|
|
444
|
+
return db.prepare(sql).all(...params) as Payment[];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function updatePaymentStatus(id: string, status: "pending" | "paid" | "failed"): Payment | null {
|
|
448
|
+
const db = getDatabase();
|
|
449
|
+
const existing = getPayment(id);
|
|
450
|
+
if (!existing) return null;
|
|
451
|
+
|
|
452
|
+
const paidAt = status === "paid" ? new Date().toISOString() : null;
|
|
453
|
+
db.prepare("UPDATE payments SET status = ?, paid_at = ? WHERE id = ?").run(status, paidAt, id);
|
|
454
|
+
|
|
455
|
+
return getPayment(id);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function deletePayment(id: string): boolean {
|
|
459
|
+
const db = getDatabase();
|
|
460
|
+
const result = db.prepare("DELETE FROM payments WHERE id = ?").run(id);
|
|
461
|
+
return result.changes > 0;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// --- Business Logic ---
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Standard deduction rates for payroll processing
|
|
468
|
+
*/
|
|
469
|
+
const DEFAULT_DEDUCTIONS = {
|
|
470
|
+
federal_tax: 0.22, // 22% federal income tax bracket
|
|
471
|
+
state_tax: 0.05, // 5% state tax
|
|
472
|
+
social_security: 0.062, // 6.2% social security
|
|
473
|
+
medicare: 0.0145, // 1.45% medicare
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Calculate deductions for a given gross pay amount.
|
|
478
|
+
* Contractors only pay federal and state tax (no FICA).
|
|
479
|
+
*/
|
|
480
|
+
export function calculateDeductions(
|
|
481
|
+
grossPay: number,
|
|
482
|
+
employeeType: "employee" | "contractor",
|
|
483
|
+
customRates?: Record<string, number>
|
|
484
|
+
): Record<string, number> {
|
|
485
|
+
const rates = customRates || DEFAULT_DEDUCTIONS;
|
|
486
|
+
const deductions: Record<string, number> = {};
|
|
487
|
+
|
|
488
|
+
if (employeeType === "contractor") {
|
|
489
|
+
// Contractors: only federal + state tax
|
|
490
|
+
deductions.federal_tax = Math.round(grossPay * (rates.federal_tax || 0.22) * 100) / 100;
|
|
491
|
+
deductions.state_tax = Math.round(grossPay * (rates.state_tax || 0.05) * 100) / 100;
|
|
492
|
+
} else {
|
|
493
|
+
// Employees: full deductions
|
|
494
|
+
for (const [key, rate] of Object.entries(rates)) {
|
|
495
|
+
deductions[key] = Math.round(grossPay * rate * 100) / 100;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return deductions;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Calculate gross pay for an employee over a pay period.
|
|
504
|
+
* For salary employees: pay_rate / 24 (semi-monthly).
|
|
505
|
+
* For hourly employees: pay_rate * hours + overtime.
|
|
506
|
+
*/
|
|
507
|
+
export function calculateGrossPay(
|
|
508
|
+
employee: Employee,
|
|
509
|
+
hoursWorked?: number,
|
|
510
|
+
overtimeHours?: number
|
|
511
|
+
): number {
|
|
512
|
+
if (employee.pay_type === "salary") {
|
|
513
|
+
// Semi-monthly: annual salary / 24
|
|
514
|
+
return Math.round((employee.pay_rate / 24) * 100) / 100;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Hourly
|
|
518
|
+
const regularHours = hoursWorked || 0;
|
|
519
|
+
const overtime = overtimeHours || 0;
|
|
520
|
+
const regularPay = regularHours * employee.pay_rate;
|
|
521
|
+
const overtimePay = overtime * employee.pay_rate * 1.5;
|
|
522
|
+
return Math.round((regularPay + overtimePay) * 100) / 100;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Process payroll for a given pay period.
|
|
527
|
+
* Auto-generates pay stubs for all active employees.
|
|
528
|
+
* Returns the generated pay stubs.
|
|
529
|
+
*/
|
|
530
|
+
export function processPayroll(
|
|
531
|
+
periodId: string,
|
|
532
|
+
hoursMap?: Record<string, { hours: number; overtime?: number }>
|
|
533
|
+
): PayStub[] {
|
|
534
|
+
const db = getDatabase();
|
|
535
|
+
const period = getPayPeriod(periodId);
|
|
536
|
+
if (!period) throw new Error(`Pay period '${periodId}' not found`);
|
|
537
|
+
if (period.status === "completed") throw new Error("Pay period already completed");
|
|
538
|
+
|
|
539
|
+
// Mark as processing
|
|
540
|
+
updatePayPeriodStatus(periodId, "processing");
|
|
541
|
+
|
|
542
|
+
const activeEmployees = listEmployees({ status: "active" });
|
|
543
|
+
const stubs: PayStub[] = [];
|
|
544
|
+
|
|
545
|
+
for (const emp of activeEmployees) {
|
|
546
|
+
// Check if stub already exists for this employee+period
|
|
547
|
+
const existing = db.prepare(
|
|
548
|
+
"SELECT id FROM pay_stubs WHERE employee_id = ? AND pay_period_id = ?"
|
|
549
|
+
).get(emp.id, periodId) as { id: string } | null;
|
|
550
|
+
if (existing) continue;
|
|
551
|
+
|
|
552
|
+
const empHours = hoursMap?.[emp.id];
|
|
553
|
+
const hoursWorked = empHours?.hours;
|
|
554
|
+
const overtimeHours = empHours?.overtime || 0;
|
|
555
|
+
|
|
556
|
+
const grossPay = calculateGrossPay(emp, hoursWorked, overtimeHours);
|
|
557
|
+
const deductions = calculateDeductions(grossPay, emp.type);
|
|
558
|
+
const totalDeductions = Object.values(deductions).reduce((sum, d) => sum + d, 0);
|
|
559
|
+
const netPay = Math.round((grossPay - totalDeductions) * 100) / 100;
|
|
560
|
+
|
|
561
|
+
const stub = createPayStub({
|
|
562
|
+
employee_id: emp.id,
|
|
563
|
+
pay_period_id: periodId,
|
|
564
|
+
gross_pay: grossPay,
|
|
565
|
+
deductions,
|
|
566
|
+
net_pay: netPay,
|
|
567
|
+
hours_worked: hoursWorked,
|
|
568
|
+
overtime_hours: overtimeHours,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
stubs.push(stub);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Mark as completed
|
|
575
|
+
updatePayPeriodStatus(periodId, "completed");
|
|
576
|
+
|
|
577
|
+
return stubs;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Get a payroll report for a pay period.
|
|
582
|
+
*/
|
|
583
|
+
export interface PayrollReport {
|
|
584
|
+
period: PayPeriod;
|
|
585
|
+
stubs: PayStub[];
|
|
586
|
+
total_gross: number;
|
|
587
|
+
total_deductions: number;
|
|
588
|
+
total_net: number;
|
|
589
|
+
employee_count: number;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export function getPayrollReport(periodId: string): PayrollReport | null {
|
|
593
|
+
const period = getPayPeriod(periodId);
|
|
594
|
+
if (!period) return null;
|
|
595
|
+
|
|
596
|
+
const stubs = listPayStubs({ pay_period_id: periodId });
|
|
597
|
+
|
|
598
|
+
const totalGross = stubs.reduce((sum, s) => sum + s.gross_pay, 0);
|
|
599
|
+
const totalDeductions = stubs.reduce((sum, s) => {
|
|
600
|
+
const d = Object.values(s.deductions).reduce((a, b) => a + b, 0);
|
|
601
|
+
return sum + d;
|
|
602
|
+
}, 0);
|
|
603
|
+
const totalNet = stubs.reduce((sum, s) => sum + s.net_pay, 0);
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
period,
|
|
607
|
+
stubs,
|
|
608
|
+
total_gross: Math.round(totalGross * 100) / 100,
|
|
609
|
+
total_deductions: Math.round(totalDeductions * 100) / 100,
|
|
610
|
+
total_net: Math.round(totalNet * 100) / 100,
|
|
611
|
+
employee_count: stubs.length,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Get year-to-date report for an employee.
|
|
617
|
+
*/
|
|
618
|
+
export interface YtdReport {
|
|
619
|
+
employee: Employee;
|
|
620
|
+
year: number;
|
|
621
|
+
total_gross: number;
|
|
622
|
+
total_deductions: Record<string, number>;
|
|
623
|
+
total_net: number;
|
|
624
|
+
pay_stubs_count: number;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function getYtdReport(employeeId: string, year?: number): YtdReport | null {
|
|
628
|
+
const employee = getEmployee(employeeId);
|
|
629
|
+
if (!employee) return null;
|
|
630
|
+
|
|
631
|
+
const targetYear = year || new Date().getFullYear();
|
|
632
|
+
const db = getDatabase();
|
|
633
|
+
|
|
634
|
+
const rows = db.prepare(
|
|
635
|
+
`SELECT ps.* FROM pay_stubs ps
|
|
636
|
+
JOIN pay_periods pp ON ps.pay_period_id = pp.id
|
|
637
|
+
WHERE ps.employee_id = ?
|
|
638
|
+
AND pp.start_date >= ? AND pp.end_date <= ?`
|
|
639
|
+
).all(
|
|
640
|
+
employeeId,
|
|
641
|
+
`${targetYear}-01-01`,
|
|
642
|
+
`${targetYear}-12-31`
|
|
643
|
+
) as PayStubRow[];
|
|
644
|
+
|
|
645
|
+
const stubs = rows.map(rowToPayStub);
|
|
646
|
+
|
|
647
|
+
const totalGross = stubs.reduce((sum, s) => sum + s.gross_pay, 0);
|
|
648
|
+
const totalNet = stubs.reduce((sum, s) => sum + s.net_pay, 0);
|
|
649
|
+
|
|
650
|
+
// Aggregate deductions by category
|
|
651
|
+
const totalDeductions: Record<string, number> = {};
|
|
652
|
+
for (const stub of stubs) {
|
|
653
|
+
for (const [key, value] of Object.entries(stub.deductions)) {
|
|
654
|
+
totalDeductions[key] = (totalDeductions[key] || 0) + value;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// Round all deduction totals
|
|
658
|
+
for (const key of Object.keys(totalDeductions)) {
|
|
659
|
+
totalDeductions[key] = Math.round(totalDeductions[key] * 100) / 100;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
employee,
|
|
664
|
+
year: targetYear,
|
|
665
|
+
total_gross: Math.round(totalGross * 100) / 100,
|
|
666
|
+
total_deductions: totalDeductions,
|
|
667
|
+
total_net: Math.round(totalNet * 100) / 100,
|
|
668
|
+
pay_stubs_count: stubs.length,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Get tax summary for all employees for a given year.
|
|
674
|
+
*/
|
|
675
|
+
export interface TaxSummaryEntry {
|
|
676
|
+
employee_id: string;
|
|
677
|
+
employee_name: string;
|
|
678
|
+
total_gross: number;
|
|
679
|
+
total_federal_tax: number;
|
|
680
|
+
total_state_tax: number;
|
|
681
|
+
total_social_security: number;
|
|
682
|
+
total_medicare: number;
|
|
683
|
+
total_deductions: number;
|
|
684
|
+
total_net: number;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function getTaxSummary(year: number): TaxSummaryEntry[] {
|
|
688
|
+
const db = getDatabase();
|
|
689
|
+
|
|
690
|
+
const employees = listEmployees();
|
|
691
|
+
const entries: TaxSummaryEntry[] = [];
|
|
692
|
+
|
|
693
|
+
for (const emp of employees) {
|
|
694
|
+
const rows = db.prepare(
|
|
695
|
+
`SELECT ps.* FROM pay_stubs ps
|
|
696
|
+
JOIN pay_periods pp ON ps.pay_period_id = pp.id
|
|
697
|
+
WHERE ps.employee_id = ?
|
|
698
|
+
AND pp.start_date >= ? AND pp.end_date <= ?`
|
|
699
|
+
).all(
|
|
700
|
+
emp.id,
|
|
701
|
+
`${year}-01-01`,
|
|
702
|
+
`${year}-12-31`
|
|
703
|
+
) as PayStubRow[];
|
|
704
|
+
|
|
705
|
+
if (rows.length === 0) continue;
|
|
706
|
+
|
|
707
|
+
const stubs = rows.map(rowToPayStub);
|
|
708
|
+
|
|
709
|
+
let totalGross = 0;
|
|
710
|
+
let totalFederalTax = 0;
|
|
711
|
+
let totalStateTax = 0;
|
|
712
|
+
let totalSocialSecurity = 0;
|
|
713
|
+
let totalMedicare = 0;
|
|
714
|
+
let totalNet = 0;
|
|
715
|
+
|
|
716
|
+
for (const stub of stubs) {
|
|
717
|
+
totalGross += stub.gross_pay;
|
|
718
|
+
totalNet += stub.net_pay;
|
|
719
|
+
totalFederalTax += stub.deductions.federal_tax || 0;
|
|
720
|
+
totalStateTax += stub.deductions.state_tax || 0;
|
|
721
|
+
totalSocialSecurity += stub.deductions.social_security || 0;
|
|
722
|
+
totalMedicare += stub.deductions.medicare || 0;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const totalDeductions = totalFederalTax + totalStateTax + totalSocialSecurity + totalMedicare;
|
|
726
|
+
|
|
727
|
+
entries.push({
|
|
728
|
+
employee_id: emp.id,
|
|
729
|
+
employee_name: emp.name,
|
|
730
|
+
total_gross: Math.round(totalGross * 100) / 100,
|
|
731
|
+
total_federal_tax: Math.round(totalFederalTax * 100) / 100,
|
|
732
|
+
total_state_tax: Math.round(totalStateTax * 100) / 100,
|
|
733
|
+
total_social_security: Math.round(totalSocialSecurity * 100) / 100,
|
|
734
|
+
total_medicare: Math.round(totalMedicare * 100) / 100,
|
|
735
|
+
total_deductions: Math.round(totalDeductions * 100) / 100,
|
|
736
|
+
total_net: Math.round(totalNet * 100) / 100,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return entries;
|
|
741
|
+
}
|