neuronix-node 0.2.0 → 0.6.0
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/dist/handlers/ap-aging.d.ts +2 -0
- package/dist/handlers/ap-aging.js +91 -0
- package/dist/handlers/ar-aging.d.ts +2 -0
- package/dist/handlers/ar-aging.js +101 -0
- package/dist/handlers/bank-reconciliation.d.ts +2 -0
- package/dist/handlers/bank-reconciliation.js +96 -0
- package/dist/handlers/budget-vs-actuals.d.ts +2 -0
- package/dist/handlers/budget-vs-actuals.js +70 -0
- package/dist/handlers/cash-flow.d.ts +2 -0
- package/dist/handlers/cash-flow.js +87 -0
- package/dist/handlers/chat.d.ts +5 -0
- package/dist/handlers/chat.js +336 -0
- package/dist/handlers/department-spending.d.ts +2 -0
- package/dist/handlers/department-spending.js +83 -0
- package/dist/handlers/depreciation.d.ts +2 -0
- package/dist/handlers/depreciation.js +93 -0
- package/dist/handlers/expense.js +166 -35
- package/dist/handlers/file-processor.d.ts +2 -2
- package/dist/handlers/file-processor.js +354 -110
- package/dist/handlers/index.d.ts +0 -6
- package/dist/handlers/index.js +26 -6
- package/dist/handlers/payroll.d.ts +2 -0
- package/dist/handlers/payroll.js +101 -0
- package/dist/handlers/sales-tax.d.ts +2 -0
- package/dist/handlers/sales-tax.js +89 -0
- package/dist/handlers/variance-analysis.d.ts +2 -0
- package/dist/handlers/variance-analysis.js +73 -0
- package/dist/handlers/w2-1099.d.ts +2 -0
- package/dist/handlers/w2-1099.js +87 -0
- package/dist/parsers/index.d.ts +2 -3
- package/dist/parsers/index.js +155 -46
- package/package.json +1 -1
package/dist/handlers/expense.js
CHANGED
|
@@ -1,38 +1,82 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.handleExpenseReport = handleExpenseReport;
|
|
4
|
+
/**
|
|
5
|
+
* Format a date string to YYYY-MM-DD for proper Excel sorting.
|
|
6
|
+
*/
|
|
7
|
+
function normalizeDate(dateStr) {
|
|
8
|
+
if (!dateStr)
|
|
9
|
+
return "";
|
|
10
|
+
// Already YYYY-MM-DD
|
|
11
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr))
|
|
12
|
+
return dateStr;
|
|
13
|
+
// M/D/YYYY or MM/DD/YYYY
|
|
14
|
+
const match = dateStr.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
|
15
|
+
if (match)
|
|
16
|
+
return `${match[3]}-${match[1].padStart(2, "0")}-${match[2].padStart(2, "0")}`;
|
|
17
|
+
return dateStr;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Format a number as currency without trailing spaces.
|
|
21
|
+
*/
|
|
22
|
+
function fmtCurrency(n) {
|
|
23
|
+
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Detect department from filename for merged file processing.
|
|
27
|
+
*/
|
|
28
|
+
function detectDepartment(fileName) {
|
|
29
|
+
const lower = fileName.toLowerCase();
|
|
30
|
+
if (/marketing/.test(lower))
|
|
31
|
+
return "Marketing";
|
|
32
|
+
if (/operation/.test(lower))
|
|
33
|
+
return "Operations";
|
|
34
|
+
if (/tech/.test(lower))
|
|
35
|
+
return "Technology";
|
|
36
|
+
if (/hr|payroll|human/.test(lower))
|
|
37
|
+
return "HR & Payroll";
|
|
38
|
+
if (/travel|entertainment/.test(lower))
|
|
39
|
+
return "Travel & Entertainment";
|
|
40
|
+
if (/sales/.test(lower))
|
|
41
|
+
return "Sales";
|
|
42
|
+
if (/admin/.test(lower))
|
|
43
|
+
return "Administration";
|
|
44
|
+
return "General";
|
|
45
|
+
}
|
|
4
46
|
async function handleExpenseReport(task) {
|
|
5
47
|
const start = Date.now();
|
|
6
48
|
const input = task.input_payload;
|
|
7
49
|
const period = input.period || "Current Period";
|
|
50
|
+
const mergedFrom = input.merged_from || [];
|
|
8
51
|
const expenses = input.expenses || [
|
|
9
52
|
{ description: "Office Supplies", amount: 245.50, category: "Operations", date: "2026-03-01" },
|
|
10
53
|
{ description: "Software Subscriptions", amount: 890.00, category: "Technology", date: "2026-03-05" },
|
|
11
54
|
{ description: "Team Lunch", amount: 156.75, category: "Meals", date: "2026-03-08" },
|
|
12
|
-
{ description: "Cloud Hosting", amount: 432.00, category: "Technology", date: "2026-03-10" },
|
|
13
|
-
{ description: "Marketing Ads", amount: 1200.00, category: "Marketing", date: "2026-03-12" },
|
|
14
|
-
{ description: "Travel - Client Meeting", amount: 387.25, category: "Travel", date: "2026-03-15" },
|
|
15
|
-
{ description: "Printer Ink", amount: 89.99, category: "Operations", date: "2026-03-18" },
|
|
16
|
-
{ description: "Conference Tickets", amount: 599.00, category: "Education", date: "2026-03-20" },
|
|
17
55
|
];
|
|
56
|
+
// Assign departments from merged file names if available
|
|
57
|
+
if (mergedFrom.length > 0 && expenses.every((e) => !e.department)) {
|
|
58
|
+
// Try to detect department from category patterns
|
|
59
|
+
for (const exp of expenses) {
|
|
60
|
+
exp.department = detectDepartmentFromCategory(exp.category);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Sort by amount descending
|
|
64
|
+
const sorted = [...expenses].sort((a, b) => b.amount - a.amount);
|
|
18
65
|
// Calculate totals by category
|
|
19
66
|
const byCategory = {};
|
|
20
67
|
let total = 0;
|
|
21
68
|
for (const exp of expenses) {
|
|
22
69
|
total += exp.amount;
|
|
23
70
|
if (!byCategory[exp.category]) {
|
|
24
|
-
byCategory[exp.category] = { total: 0, count: 0
|
|
71
|
+
byCategory[exp.category] = { total: 0, count: 0 };
|
|
25
72
|
}
|
|
26
73
|
byCategory[exp.category].total += exp.amount;
|
|
27
74
|
byCategory[exp.category].count += 1;
|
|
28
|
-
byCategory[exp.category].items.push(exp);
|
|
29
75
|
}
|
|
30
|
-
// Round totals
|
|
31
76
|
total = Math.round(total * 100) / 100;
|
|
32
77
|
for (const cat of Object.keys(byCategory)) {
|
|
33
78
|
byCategory[cat].total = Math.round(byCategory[cat].total * 100) / 100;
|
|
34
79
|
}
|
|
35
|
-
// Sort categories by total descending
|
|
36
80
|
const sortedCategories = Object.entries(byCategory)
|
|
37
81
|
.sort((a, b) => b[1].total - a[1].total)
|
|
38
82
|
.map(([name, data]) => ({
|
|
@@ -41,41 +85,110 @@ async function handleExpenseReport(task) {
|
|
|
41
85
|
count: data.count,
|
|
42
86
|
percentage: Math.round((data.total / total) * 1000) / 10,
|
|
43
87
|
}));
|
|
44
|
-
//
|
|
88
|
+
// Calculate by department if available
|
|
89
|
+
const hasDepartments = sorted.some((e) => e.department);
|
|
90
|
+
const byDepartment = {};
|
|
91
|
+
if (hasDepartments) {
|
|
92
|
+
for (const exp of expenses) {
|
|
93
|
+
const dept = exp.department || "General";
|
|
94
|
+
if (!byDepartment[dept])
|
|
95
|
+
byDepartment[dept] = { total: 0, count: 0 };
|
|
96
|
+
byDepartment[dept].total += exp.amount;
|
|
97
|
+
byDepartment[dept].count += 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const sortedDepartments = Object.entries(byDepartment)
|
|
101
|
+
.sort((a, b) => b[1].total - a[1].total)
|
|
102
|
+
.map(([name, data]) => ({
|
|
103
|
+
department: name,
|
|
104
|
+
total: Math.round(data.total * 100) / 100,
|
|
105
|
+
count: data.count,
|
|
106
|
+
percentage: Math.round((data.total / total) * 1000) / 10,
|
|
107
|
+
}));
|
|
108
|
+
// Chart data
|
|
45
109
|
const chartData = {
|
|
46
110
|
chart_type: "doughnut",
|
|
47
111
|
title: `Expenses by Category — ${period}`,
|
|
48
112
|
labels: sortedCategories.map((c) => c.category),
|
|
49
|
-
datasets: [{
|
|
50
|
-
label: "Expenses",
|
|
51
|
-
data: sortedCategories.map((c) => c.total),
|
|
52
|
-
}],
|
|
113
|
+
datasets: [{ label: "Expenses", data: sortedCategories.map((c) => c.total) }],
|
|
53
114
|
};
|
|
54
115
|
// Summary text
|
|
55
116
|
const summaryLines = [
|
|
56
117
|
`Expense Report: ${period}`,
|
|
57
|
-
`Total:
|
|
118
|
+
`Total: ${fmtCurrency(total)} across ${expenses.length} expenses in ${sortedCategories.length} categories`,
|
|
58
119
|
``,
|
|
59
|
-
`
|
|
60
|
-
...sortedCategories.map((c) => ` ${c.category}:
|
|
120
|
+
`Top Categories:`,
|
|
121
|
+
...sortedCategories.slice(0, 10).map((c) => ` ${c.category}: ${fmtCurrency(c.total)} (${c.percentage}%) — ${c.count} items`),
|
|
61
122
|
``,
|
|
62
|
-
`Largest expense: ${
|
|
63
|
-
|
|
64
|
-
// Generate CSV output
|
|
65
|
-
const csvLines = [
|
|
66
|
-
"EXPENSE REPORT",
|
|
67
|
-
`Period,${period}`,
|
|
68
|
-
"",
|
|
69
|
-
"EXPENSES BY ITEM",
|
|
70
|
-
"Date,Description,Amount,Category",
|
|
71
|
-
...expenses.map((e) => `${e.date || ""},${csvEscape(e.description)},$${e.amount.toFixed(2)},${csvEscape(e.category)}`),
|
|
72
|
-
"",
|
|
73
|
-
"SUMMARY BY CATEGORY",
|
|
74
|
-
"Category,Total,% of Total,Item Count",
|
|
75
|
-
...sortedCategories.map((c) => `${csvEscape(c.category)},$${c.total.toFixed(2)},${c.percentage}%,${c.count}`),
|
|
76
|
-
"",
|
|
77
|
-
`TOTAL,,$${total.toFixed(2)},${expenses.length} items`,
|
|
123
|
+
`Largest expense: ${sorted[0].description} (${fmtCurrency(sorted[0].amount)})`,
|
|
124
|
+
`Smallest expense: ${sorted[sorted.length - 1].description} (${fmtCurrency(sorted[sorted.length - 1].amount)})`,
|
|
78
125
|
];
|
|
126
|
+
// ── Build clean CSV output ────────────────────────────────
|
|
127
|
+
const csvLines = [];
|
|
128
|
+
const hasDept = hasDepartments;
|
|
129
|
+
const itemHeaders = hasDept
|
|
130
|
+
? "Date,Department,Description,Category,Amount"
|
|
131
|
+
: "Date,Description,Category,Amount";
|
|
132
|
+
// Report header
|
|
133
|
+
csvLines.push("EXPENSE REPORT");
|
|
134
|
+
csvLines.push(`Period: ${period}`);
|
|
135
|
+
csvLines.push(`Generated: ${new Date().toISOString().split("T")[0]}`);
|
|
136
|
+
if (mergedFrom.length > 0) {
|
|
137
|
+
csvLines.push(`Sources: ${mergedFrom.length} files merged`);
|
|
138
|
+
}
|
|
139
|
+
csvLines.push(`Total Items: ${expenses.length}`);
|
|
140
|
+
csvLines.push(`Grand Total: ${fmtCurrency(total)}`);
|
|
141
|
+
csvLines.push("");
|
|
142
|
+
// Section 1: All expenses sorted by amount
|
|
143
|
+
csvLines.push("DETAILED EXPENSES (sorted by amount)");
|
|
144
|
+
csvLines.push(itemHeaders);
|
|
145
|
+
for (const exp of sorted) {
|
|
146
|
+
const date = normalizeDate(exp.date);
|
|
147
|
+
const desc = csvEscape(exp.description);
|
|
148
|
+
const cat = csvEscape(exp.category);
|
|
149
|
+
const amt = fmtCurrency(exp.amount);
|
|
150
|
+
if (hasDept) {
|
|
151
|
+
csvLines.push(`${date},${csvEscape(exp.department || "General")},${desc},${cat},${amt}`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
csvLines.push(`${date},${desc},${cat},${amt}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Subtotal row
|
|
158
|
+
if (hasDept) {
|
|
159
|
+
csvLines.push(`,,,,`);
|
|
160
|
+
csvLines.push(`,,,SUBTOTAL,${fmtCurrency(total)}`);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
csvLines.push(`,,,`);
|
|
164
|
+
csvLines.push(`,,SUBTOTAL,${fmtCurrency(total)}`);
|
|
165
|
+
}
|
|
166
|
+
csvLines.push("");
|
|
167
|
+
// Section 2: Summary by category
|
|
168
|
+
csvLines.push("SUMMARY BY CATEGORY");
|
|
169
|
+
csvLines.push("Category,Amount,% of Total,Item Count");
|
|
170
|
+
for (const c of sortedCategories) {
|
|
171
|
+
csvLines.push(`${csvEscape(c.category)},${fmtCurrency(c.total)},${c.percentage}%,${c.count}`);
|
|
172
|
+
}
|
|
173
|
+
csvLines.push("");
|
|
174
|
+
// Section 3: Summary by department (if merged)
|
|
175
|
+
if (hasDept && sortedDepartments.length > 0) {
|
|
176
|
+
csvLines.push("SUMMARY BY DEPARTMENT");
|
|
177
|
+
csvLines.push("Department,Amount,% of Total,Item Count");
|
|
178
|
+
for (const d of sortedDepartments) {
|
|
179
|
+
csvLines.push(`${csvEscape(d.department)},${fmtCurrency(d.total)},${d.percentage}%,${d.count}`);
|
|
180
|
+
}
|
|
181
|
+
csvLines.push("");
|
|
182
|
+
}
|
|
183
|
+
// Grand total
|
|
184
|
+
csvLines.push("GRAND TOTAL");
|
|
185
|
+
csvLines.push(`Total Expenses,${fmtCurrency(total)}`);
|
|
186
|
+
csvLines.push(`Total Items,${expenses.length}`);
|
|
187
|
+
csvLines.push(`Total Categories,${sortedCategories.length}`);
|
|
188
|
+
if (hasDept)
|
|
189
|
+
csvLines.push(`Total Departments,${sortedDepartments.length}`);
|
|
190
|
+
csvLines.push(`Largest Expense,${csvEscape(sorted[0].description)} (${fmtCurrency(sorted[0].amount)})`);
|
|
191
|
+
csvLines.push(`Average Expense,${fmtCurrency(Math.round((total / expenses.length) * 100) / 100)}`);
|
|
79
192
|
const durationMs = Date.now() - start;
|
|
80
193
|
return {
|
|
81
194
|
text: summaryLines.join("\n"),
|
|
@@ -86,10 +199,12 @@ async function handleExpenseReport(task) {
|
|
|
86
199
|
expense_count: expenses.length,
|
|
87
200
|
category_count: sortedCategories.length,
|
|
88
201
|
largest_category: sortedCategories[0]?.category,
|
|
89
|
-
largest_expense:
|
|
202
|
+
largest_expense: sorted[0],
|
|
203
|
+
average_expense: Math.round((total / expenses.length) * 100) / 100,
|
|
90
204
|
},
|
|
91
205
|
by_category: sortedCategories,
|
|
92
|
-
|
|
206
|
+
by_department: sortedDepartments.length > 0 ? sortedDepartments : undefined,
|
|
207
|
+
expenses: sorted,
|
|
93
208
|
chart_data: chartData,
|
|
94
209
|
duration_ms: durationMs,
|
|
95
210
|
};
|
|
@@ -100,3 +215,19 @@ function csvEscape(value) {
|
|
|
100
215
|
}
|
|
101
216
|
return value;
|
|
102
217
|
}
|
|
218
|
+
function detectDepartmentFromCategory(category) {
|
|
219
|
+
const lower = category.toLowerCase();
|
|
220
|
+
if (/payroll|benefits|recruiting|onboarding|training|employee|hr|insurance|safety/.test(lower))
|
|
221
|
+
return "HR & Payroll";
|
|
222
|
+
if (/digital|marketing|pr|creative|print|email|influencer|affiliate|content|seo|advertising/.test(lower))
|
|
223
|
+
return "Marketing";
|
|
224
|
+
if (/cloud|software|hardware|ai|infrastructure|security/.test(lower))
|
|
225
|
+
return "Technology";
|
|
226
|
+
if (/rent|utilities|maintenance|office supplies|equipment|furniture/.test(lower))
|
|
227
|
+
return "Operations";
|
|
228
|
+
if (/airfare|lodging|hotel|transport|conference|entertainment|team meal/.test(lower))
|
|
229
|
+
return "Travel & Entertainment";
|
|
230
|
+
if (/consulting/.test(lower))
|
|
231
|
+
return "Consulting";
|
|
232
|
+
return "General";
|
|
233
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TaskInput, TaskOutput } from "./index.js";
|
|
2
2
|
/**
|
|
3
3
|
* File processor handler.
|
|
4
|
-
* Receives a file's content, parses it, determines
|
|
5
|
-
*
|
|
4
|
+
* Receives a file's content, parses it, determines the best handler,
|
|
5
|
+
* converts CSV data into the right format, and routes it.
|
|
6
6
|
*/
|
|
7
7
|
export declare function handleFileProcess(task: TaskInput): Promise<TaskOutput>;
|