neuronix-node 0.5.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/file-processor.d.ts +2 -2
- package/dist/handlers/file-processor.js +353 -109
- package/dist/parsers/index.d.ts +2 -3
- package/dist/parsers/index.js +155 -46
- package/package.json +1 -1
|
@@ -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>;
|
|
@@ -5,11 +5,18 @@ const index_js_1 = require("../parsers/index.js");
|
|
|
5
5
|
const chart_js_1 = require("./chart.js");
|
|
6
6
|
const expense_js_1 = require("./expense.js");
|
|
7
7
|
const pnl_js_1 = require("./pnl.js");
|
|
8
|
-
const
|
|
8
|
+
const ar_aging_js_1 = require("./ar-aging.js");
|
|
9
|
+
const ap_aging_js_1 = require("./ap-aging.js");
|
|
10
|
+
const bank_reconciliation_js_1 = require("./bank-reconciliation.js");
|
|
11
|
+
const budget_vs_actuals_js_1 = require("./budget-vs-actuals.js");
|
|
12
|
+
const payroll_js_1 = require("./payroll.js");
|
|
13
|
+
const sales_tax_js_1 = require("./sales-tax.js");
|
|
14
|
+
const depreciation_js_1 = require("./depreciation.js");
|
|
15
|
+
const department_spending_js_1 = require("./department-spending.js");
|
|
9
16
|
/**
|
|
10
17
|
* File processor handler.
|
|
11
|
-
* Receives a file's content, parses it, determines
|
|
12
|
-
*
|
|
18
|
+
* Receives a file's content, parses it, determines the best handler,
|
|
19
|
+
* converts CSV data into the right format, and routes it.
|
|
13
20
|
*/
|
|
14
21
|
async function handleFileProcess(task) {
|
|
15
22
|
const start = Date.now();
|
|
@@ -17,141 +24,358 @@ async function handleFileProcess(task) {
|
|
|
17
24
|
const fileName = payload.file_name || "unknown";
|
|
18
25
|
const fileType = payload.file_type || "unknown";
|
|
19
26
|
const content = payload.file_content || "";
|
|
20
|
-
// Parse the file
|
|
27
|
+
// Parse the file and detect type
|
|
21
28
|
const parsed = (0, index_js_1.parseFile)(fileName, fileType, content);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
if (parsed.suggestedActions.length === 0 || parsed.type !== "csv") {
|
|
30
|
+
return {
|
|
31
|
+
text: `Parsed "${fileName}" (${fileType}). Could not determine the appropriate handler.\n\n${JSON.stringify(parsed.data, null, 2)}`,
|
|
32
|
+
parsed_file: parsed,
|
|
33
|
+
duration_ms: Date.now() - start,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const bestAction = parsed.suggestedActions[0];
|
|
37
|
+
const csvData = parsed.data;
|
|
38
|
+
const headers = csvData.headers.map(h => h.toLowerCase());
|
|
39
|
+
try {
|
|
40
|
+
let result;
|
|
41
|
+
switch (bestAction) {
|
|
42
|
+
case "expense_report": {
|
|
43
|
+
const expenses = csvToExpenses(csvData);
|
|
44
|
+
if (expenses.length === 0)
|
|
45
|
+
break;
|
|
46
|
+
result = await (0, expense_js_1.handleExpenseReport)({
|
|
31
47
|
type: "expense_report",
|
|
32
48
|
input_payload: { expenses, merged_from: payload.merged_from },
|
|
33
49
|
});
|
|
34
|
-
return
|
|
35
|
-
...result,
|
|
36
|
-
parsed_file: { type: parsed.type, rows: csvData.rowCount, columns: csvData.headers.length },
|
|
37
|
-
auto_action: bestAction,
|
|
38
|
-
duration_ms: Date.now() - start,
|
|
39
|
-
};
|
|
50
|
+
return withMeta(result, parsed, bestAction, start);
|
|
40
51
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
input_payload: chartInput,
|
|
52
|
+
case "ar_aging": {
|
|
53
|
+
const invoices = csvToInvoices(csvData);
|
|
54
|
+
if (invoices.length === 0)
|
|
55
|
+
break;
|
|
56
|
+
result = await (0, ar_aging_js_1.handleARAging)({
|
|
57
|
+
type: "ar_aging",
|
|
58
|
+
input_payload: { invoices },
|
|
49
59
|
});
|
|
50
|
-
return
|
|
51
|
-
...result,
|
|
52
|
-
parsed_file: { type: parsed.type, rows: csvData.rowCount, columns: csvData.headers.length },
|
|
53
|
-
auto_action: bestAction,
|
|
54
|
-
duration_ms: Date.now() - start,
|
|
55
|
-
};
|
|
60
|
+
return withMeta(result, parsed, bestAction, start);
|
|
56
61
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
case "ap_aging": {
|
|
63
|
+
const bills = csvToBills(csvData);
|
|
64
|
+
if (bills.length === 0)
|
|
65
|
+
break;
|
|
66
|
+
result = await (0, ap_aging_js_1.handleAPAging)({
|
|
67
|
+
type: "ap_aging",
|
|
68
|
+
input_payload: { bills },
|
|
69
|
+
});
|
|
70
|
+
return withMeta(result, parsed, bestAction, start);
|
|
71
|
+
}
|
|
72
|
+
case "bank_reconciliation": {
|
|
73
|
+
const bankTxns = csvToBankTransactions(csvData);
|
|
74
|
+
if (bankTxns.length === 0)
|
|
75
|
+
break;
|
|
76
|
+
result = await (0, bank_reconciliation_js_1.handleBankReconciliation)({
|
|
77
|
+
type: "bank_reconciliation",
|
|
78
|
+
input_payload: { bank_transactions: bankTxns },
|
|
79
|
+
});
|
|
80
|
+
return withMeta(result, parsed, bestAction, start);
|
|
81
|
+
}
|
|
82
|
+
case "budget_vs_actuals": {
|
|
83
|
+
const lines = csvToBudget(csvData);
|
|
84
|
+
if (lines.length === 0)
|
|
85
|
+
break;
|
|
86
|
+
result = await (0, budget_vs_actuals_js_1.handleBudgetVsActuals)({
|
|
87
|
+
type: "budget_vs_actuals",
|
|
88
|
+
input_payload: { lines },
|
|
89
|
+
});
|
|
90
|
+
return withMeta(result, parsed, bestAction, start);
|
|
91
|
+
}
|
|
92
|
+
case "payroll": {
|
|
93
|
+
const employees = csvToPayroll(csvData);
|
|
94
|
+
if (employees.length === 0)
|
|
95
|
+
break;
|
|
96
|
+
result = await (0, payroll_js_1.handlePayroll)({
|
|
97
|
+
type: "payroll",
|
|
98
|
+
input_payload: { employees },
|
|
99
|
+
});
|
|
100
|
+
return withMeta(result, parsed, bestAction, start);
|
|
101
|
+
}
|
|
102
|
+
case "sales_tax": {
|
|
103
|
+
const transactions = csvToSalesTax(csvData);
|
|
104
|
+
if (transactions.length === 0)
|
|
105
|
+
break;
|
|
106
|
+
result = await (0, sales_tax_js_1.handleSalesTax)({
|
|
107
|
+
type: "sales_tax",
|
|
108
|
+
input_payload: { transactions },
|
|
109
|
+
});
|
|
110
|
+
return withMeta(result, parsed, bestAction, start);
|
|
111
|
+
}
|
|
112
|
+
case "depreciation": {
|
|
113
|
+
const assets = csvToAssets(csvData);
|
|
114
|
+
if (assets.length === 0)
|
|
115
|
+
break;
|
|
116
|
+
result = await (0, depreciation_js_1.handleDepreciation)({
|
|
117
|
+
type: "depreciation",
|
|
118
|
+
input_payload: { assets },
|
|
119
|
+
});
|
|
120
|
+
return withMeta(result, parsed, bestAction, start);
|
|
121
|
+
}
|
|
122
|
+
case "department_spending": {
|
|
123
|
+
const expenses = csvToDeptExpenses(csvData);
|
|
124
|
+
if (expenses.length === 0)
|
|
125
|
+
break;
|
|
126
|
+
result = await (0, department_spending_js_1.handleDepartmentSpending)({
|
|
127
|
+
type: "department_spending",
|
|
128
|
+
input_payload: { expenses },
|
|
129
|
+
});
|
|
130
|
+
return withMeta(result, parsed, bestAction, start);
|
|
131
|
+
}
|
|
132
|
+
case "pnl": {
|
|
133
|
+
const pnlInput = csvToPnl(csvData);
|
|
134
|
+
if (!pnlInput)
|
|
135
|
+
break;
|
|
136
|
+
result = await (0, pnl_js_1.handlePnl)({
|
|
63
137
|
type: "pnl",
|
|
64
138
|
input_payload: pnlInput,
|
|
65
139
|
});
|
|
66
|
-
return
|
|
67
|
-
...result,
|
|
68
|
-
parsed_file: { type: parsed.type, rows: csvData.rowCount, columns: csvData.headers.length },
|
|
69
|
-
auto_action: bestAction,
|
|
70
|
-
duration_ms: Date.now() - start,
|
|
71
|
-
};
|
|
140
|
+
return withMeta(result, parsed, bestAction, start);
|
|
72
141
|
}
|
|
142
|
+
case "chart": {
|
|
143
|
+
const chartInput = csvToChart(csvData, fileName);
|
|
144
|
+
if (!chartInput)
|
|
145
|
+
break;
|
|
146
|
+
result = await (0, chart_js_1.handleChart)({
|
|
147
|
+
type: "chart",
|
|
148
|
+
input_payload: chartInput,
|
|
149
|
+
});
|
|
150
|
+
return withMeta(result, parsed, bestAction, start);
|
|
151
|
+
}
|
|
152
|
+
default:
|
|
153
|
+
break;
|
|
73
154
|
}
|
|
74
|
-
if (bestAction === "invoice") {
|
|
75
|
-
const result = await (0, invoice_js_1.handleInvoice)({
|
|
76
|
-
type: "invoice",
|
|
77
|
-
input_payload: {},
|
|
78
|
-
});
|
|
79
|
-
return {
|
|
80
|
-
...result,
|
|
81
|
-
parsed_file: { type: parsed.type },
|
|
82
|
-
auto_action: bestAction,
|
|
83
|
-
duration_ms: Date.now() - start,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
155
|
}
|
|
87
|
-
|
|
88
|
-
|
|
156
|
+
catch (err) {
|
|
157
|
+
return {
|
|
158
|
+
text: `Error processing "${fileName}" as ${bestAction}: ${err}`,
|
|
159
|
+
parsed_file: parsed,
|
|
160
|
+
auto_action: bestAction,
|
|
161
|
+
error: String(err),
|
|
162
|
+
duration_ms: Date.now() - start,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// Fallback: return parsed data with a chart
|
|
166
|
+
const chartInput = csvToChart(csvData, fileName);
|
|
167
|
+
if (chartInput) {
|
|
168
|
+
const result = await (0, chart_js_1.handleChart)({ type: "chart", input_payload: chartInput });
|
|
169
|
+
return withMeta(result, parsed, "chart", start);
|
|
170
|
+
}
|
|
89
171
|
return {
|
|
90
|
-
text: `Parsed "${fileName}"
|
|
172
|
+
text: `Parsed "${fileName}" — ${csvData.rowCount} rows, ${csvData.headers.length} columns.\n\nHeaders: ${csvData.headers.join(", ")}`,
|
|
91
173
|
parsed_file: parsed,
|
|
92
|
-
|
|
93
|
-
duration_ms: durationMs,
|
|
174
|
+
duration_ms: Date.now() - start,
|
|
94
175
|
};
|
|
95
176
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
177
|
+
function withMeta(result, parsed, action, start) {
|
|
178
|
+
const csv = parsed.data;
|
|
179
|
+
return {
|
|
180
|
+
...result,
|
|
181
|
+
parsed_file: { type: parsed.type, rows: csv.rowCount || 0, columns: csv.headers?.length || 0 },
|
|
182
|
+
auto_action: action,
|
|
183
|
+
duration_ms: Date.now() - start,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// ═══════════════════════════════════════════════════════════
|
|
187
|
+
// CSV → Handler Data Converters
|
|
188
|
+
// ═══════════════════════════════════════════════════════════
|
|
189
|
+
function findCol(headers, ...patterns) {
|
|
190
|
+
for (const p of patterns) {
|
|
191
|
+
const idx = headers.findIndex(h => p.test(h));
|
|
192
|
+
if (idx >= 0)
|
|
193
|
+
return idx;
|
|
194
|
+
}
|
|
195
|
+
return -1;
|
|
196
|
+
}
|
|
197
|
+
function parseNum(val) {
|
|
198
|
+
return parseFloat((val || "0").replace(/[$,]/g, "")) || 0;
|
|
199
|
+
}
|
|
99
200
|
function csvToExpenses(csv) {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const dateCol = headersLower.findIndex((h) => /date/.test(h));
|
|
201
|
+
const h = csv.headers.map(h => h.toLowerCase());
|
|
202
|
+
const descCol = findCol(h, /description|name|item|memo/);
|
|
203
|
+
const amountCol = findCol(h, /amount|cost|price|total/);
|
|
204
|
+
const categoryCol = findCol(h, /category|type|class|department/);
|
|
205
|
+
const dateCol = findCol(h, /date/);
|
|
106
206
|
if (amountCol === -1)
|
|
107
207
|
return [];
|
|
108
|
-
return csv.rows.map(
|
|
109
|
-
description: descCol >= 0 ?
|
|
110
|
-
amount:
|
|
111
|
-
category: categoryCol >= 0 ?
|
|
112
|
-
date: dateCol >= 0 ?
|
|
113
|
-
})).filter(
|
|
208
|
+
return csv.rows.map(r => ({
|
|
209
|
+
description: descCol >= 0 ? r[descCol] : "Item",
|
|
210
|
+
amount: parseNum(r[amountCol]),
|
|
211
|
+
category: categoryCol >= 0 ? r[categoryCol] : "Uncategorized",
|
|
212
|
+
date: dateCol >= 0 ? r[dateCol] : undefined,
|
|
213
|
+
})).filter(e => e.amount > 0);
|
|
114
214
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
215
|
+
function csvToInvoices(csv) {
|
|
216
|
+
const h = csv.headers.map(h => h.toLowerCase());
|
|
217
|
+
const customerCol = findCol(h, /customer|client|bill.?to/);
|
|
218
|
+
const invNumCol = findCol(h, /invoice|inv/);
|
|
219
|
+
const amountCol = findCol(h, /amount|total/);
|
|
220
|
+
const dateIssuedCol = findCol(h, /date.?issued|issue.?date|^date$/);
|
|
221
|
+
const dateDueCol = findCol(h, /due.?date|payment.?due/);
|
|
222
|
+
const statusCol = findCol(h, /status/);
|
|
223
|
+
const paidCol = findCol(h, /paid|payment/);
|
|
224
|
+
if (amountCol === -1)
|
|
225
|
+
return [];
|
|
226
|
+
return csv.rows.map(r => {
|
|
227
|
+
const status = statusCol >= 0 ? r[statusCol]?.toLowerCase() || "" : "";
|
|
228
|
+
const paid = paidCol >= 0 ? parseNum(r[paidCol]) : (status === "paid" ? parseNum(r[amountCol]) : 0);
|
|
229
|
+
return {
|
|
230
|
+
customer: customerCol >= 0 ? r[customerCol] : "Unknown",
|
|
231
|
+
invoice_number: invNumCol >= 0 ? r[invNumCol] : undefined,
|
|
232
|
+
amount: parseNum(r[amountCol]),
|
|
233
|
+
date_issued: dateIssuedCol >= 0 ? r[dateIssuedCol] : new Date().toISOString().split("T")[0],
|
|
234
|
+
date_due: dateDueCol >= 0 ? r[dateDueCol] : new Date().toISOString().split("T")[0],
|
|
235
|
+
amount_paid: paid,
|
|
236
|
+
};
|
|
237
|
+
}).filter(e => e.amount > 0);
|
|
238
|
+
}
|
|
239
|
+
function csvToBills(csv) {
|
|
240
|
+
const h = csv.headers.map(h => h.toLowerCase());
|
|
241
|
+
const vendorCol = findCol(h, /vendor|supplier|payee/);
|
|
242
|
+
const billNumCol = findCol(h, /bill|inv/);
|
|
243
|
+
const amountCol = findCol(h, /amount|total/);
|
|
244
|
+
const dateRecCol = findCol(h, /date.?received|receive|^date$/);
|
|
245
|
+
const dateDueCol = findCol(h, /due.?date/);
|
|
246
|
+
const paidCol = findCol(h, /paid|payment/);
|
|
247
|
+
if (amountCol === -1)
|
|
248
|
+
return [];
|
|
249
|
+
return csv.rows.map(r => ({
|
|
250
|
+
vendor: vendorCol >= 0 ? r[vendorCol] : "Unknown",
|
|
251
|
+
bill_number: billNumCol >= 0 ? r[billNumCol] : undefined,
|
|
252
|
+
amount: parseNum(r[amountCol]),
|
|
253
|
+
date_received: dateRecCol >= 0 ? r[dateRecCol] : new Date().toISOString().split("T")[0],
|
|
254
|
+
date_due: dateDueCol >= 0 ? r[dateDueCol] : new Date().toISOString().split("T")[0],
|
|
255
|
+
amount_paid: paidCol >= 0 ? parseNum(r[paidCol]) : 0,
|
|
256
|
+
})).filter(e => e.amount > 0);
|
|
257
|
+
}
|
|
258
|
+
function csvToBankTransactions(csv) {
|
|
259
|
+
const h = csv.headers.map(h => h.toLowerCase());
|
|
260
|
+
const dateCol = findCol(h, /date/);
|
|
261
|
+
const descCol = findCol(h, /description|desc|memo|payee/);
|
|
262
|
+
const amountCol = findCol(h, /amount|total/);
|
|
263
|
+
const typeCol = findCol(h, /^type$/);
|
|
264
|
+
const refCol = findCol(h, /reference|ref|check/);
|
|
265
|
+
if (amountCol === -1)
|
|
266
|
+
return [];
|
|
267
|
+
return csv.rows.map(r => {
|
|
268
|
+
const amount = parseNum(r[amountCol]);
|
|
269
|
+
let type = amount >= 0 ? "credit" : "debit";
|
|
270
|
+
if (typeCol >= 0) {
|
|
271
|
+
const t = (r[typeCol] || "").toLowerCase();
|
|
272
|
+
if (t === "debit")
|
|
273
|
+
type = "debit";
|
|
274
|
+
else if (t === "credit")
|
|
275
|
+
type = "credit";
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
date: dateCol >= 0 ? r[dateCol] : "",
|
|
279
|
+
description: descCol >= 0 ? r[descCol] : "",
|
|
280
|
+
amount: Math.abs(amount),
|
|
281
|
+
type,
|
|
282
|
+
reference: refCol >= 0 ? r[refCol] : undefined,
|
|
283
|
+
};
|
|
284
|
+
}).filter(t => t.amount > 0);
|
|
285
|
+
}
|
|
286
|
+
function csvToBudget(csv) {
|
|
287
|
+
const h = csv.headers.map(h => h.toLowerCase());
|
|
288
|
+
const catCol = findCol(h, /category|item|line|department|account/);
|
|
289
|
+
const budgetCol = findCol(h, /budget|planned|forecast/);
|
|
290
|
+
const actualCol = findCol(h, /actual|spent|real/);
|
|
291
|
+
if (budgetCol === -1 || actualCol === -1)
|
|
292
|
+
return [];
|
|
293
|
+
return csv.rows.map(r => ({
|
|
294
|
+
category: catCol >= 0 ? r[catCol] : "Item",
|
|
295
|
+
budget: parseNum(r[budgetCol]),
|
|
296
|
+
actual: parseNum(r[actualCol]),
|
|
297
|
+
})).filter(l => l.budget > 0 || l.actual > 0);
|
|
298
|
+
}
|
|
299
|
+
function csvToPayroll(csv) {
|
|
300
|
+
const h = csv.headers.map(h => h.toLowerCase());
|
|
301
|
+
const nameCol = findCol(h, /employee|name/);
|
|
302
|
+
const typeCol = findCol(h, /^type$/);
|
|
303
|
+
const rateCol = findCol(h, /rate|salary|wage|pay/);
|
|
304
|
+
const hoursCol = findCol(h, /hours/);
|
|
305
|
+
const deptCol = findCol(h, /department|dept/);
|
|
306
|
+
if (nameCol === -1 || rateCol === -1)
|
|
307
|
+
return [];
|
|
308
|
+
return csv.rows.map(r => {
|
|
309
|
+
const type = typeCol >= 0 ? (r[typeCol]?.toLowerCase().includes("hourly") ? "hourly" : "salary") : "salary";
|
|
310
|
+
return {
|
|
311
|
+
name: r[nameCol] || "Employee",
|
|
312
|
+
type: type,
|
|
313
|
+
rate: parseNum(r[rateCol]),
|
|
314
|
+
hours: hoursCol >= 0 && r[hoursCol] ? parseNum(r[hoursCol]) : undefined,
|
|
315
|
+
};
|
|
316
|
+
}).filter(e => e.rate > 0);
|
|
317
|
+
}
|
|
318
|
+
function csvToSalesTax(csv) {
|
|
319
|
+
const h = csv.headers.map(h => h.toLowerCase());
|
|
320
|
+
const dateCol = findCol(h, /date/);
|
|
321
|
+
const descCol = findCol(h, /description|item/);
|
|
322
|
+
const amountCol = findCol(h, /amount|total|sales/);
|
|
323
|
+
const taxRateCol = findCol(h, /tax.?rate/);
|
|
324
|
+
const stateCol = findCol(h, /state/);
|
|
325
|
+
const taxableCol = findCol(h, /taxable/);
|
|
326
|
+
if (amountCol === -1)
|
|
327
|
+
return [];
|
|
328
|
+
return csv.rows.map(r => ({
|
|
329
|
+
date: dateCol >= 0 ? r[dateCol] : "",
|
|
330
|
+
description: descCol >= 0 ? r[descCol] : "Sale",
|
|
331
|
+
amount: parseNum(r[amountCol]),
|
|
332
|
+
tax_rate: taxRateCol >= 0 ? parseNum(r[taxRateCol]) : 0,
|
|
333
|
+
state: stateCol >= 0 ? r[stateCol] : undefined,
|
|
334
|
+
taxable: taxableCol >= 0 ? /yes|true|1/i.test(r[taxableCol]) : true,
|
|
335
|
+
})).filter(t => t.amount > 0);
|
|
336
|
+
}
|
|
337
|
+
function csvToAssets(csv) {
|
|
338
|
+
const h = csv.headers.map(h => h.toLowerCase());
|
|
339
|
+
const nameCol = findCol(h, /name|asset|description/);
|
|
340
|
+
const costCol = findCol(h, /cost|price|value/);
|
|
341
|
+
const salvageCol = findCol(h, /salvage|residual/);
|
|
342
|
+
const lifeCol = findCol(h, /useful.?life|lifespan|years/);
|
|
343
|
+
const acquiredCol = findCol(h, /acquired|purchase|date/);
|
|
344
|
+
if (costCol === -1)
|
|
345
|
+
return [];
|
|
346
|
+
return csv.rows.map(r => ({
|
|
347
|
+
name: nameCol >= 0 ? r[nameCol] : "Asset",
|
|
348
|
+
cost: parseNum(r[costCol]),
|
|
349
|
+
salvage_value: salvageCol >= 0 ? parseNum(r[salvageCol]) : 0,
|
|
350
|
+
useful_life_years: lifeCol >= 0 ? parseNum(r[lifeCol]) : 5,
|
|
351
|
+
date_acquired: acquiredCol >= 0 ? r[acquiredCol] : "2024-01-01",
|
|
352
|
+
})).filter(a => a.cost > 0);
|
|
353
|
+
}
|
|
354
|
+
function csvToDeptExpenses(csv) {
|
|
355
|
+
const h = csv.headers.map(h => h.toLowerCase());
|
|
356
|
+
const deptCol = findCol(h, /department|dept|team|division/);
|
|
357
|
+
const catCol = findCol(h, /category|type|class/);
|
|
358
|
+
const amountCol = findCol(h, /amount|cost|total/);
|
|
359
|
+
if (deptCol === -1 || amountCol === -1)
|
|
360
|
+
return [];
|
|
361
|
+
return csv.rows.map(r => ({
|
|
362
|
+
department: r[deptCol] || "General",
|
|
363
|
+
category: catCol >= 0 ? r[catCol] : "General",
|
|
364
|
+
amount: parseNum(r[amountCol]),
|
|
365
|
+
})).filter(e => e.amount > 0);
|
|
139
366
|
}
|
|
140
|
-
/**
|
|
141
|
-
* Convert CSV data into P&L parameters.
|
|
142
|
-
*/
|
|
143
367
|
function csvToPnl(csv) {
|
|
144
|
-
const
|
|
145
|
-
const descCol =
|
|
146
|
-
const amountCol =
|
|
147
|
-
const typeCol =
|
|
368
|
+
const h = csv.headers.map(h => h.toLowerCase());
|
|
369
|
+
const descCol = findCol(h, /description|name|item/);
|
|
370
|
+
const amountCol = findCol(h, /amount|total/);
|
|
371
|
+
const typeCol = findCol(h, /type|category/);
|
|
148
372
|
if (amountCol === -1)
|
|
149
373
|
return null;
|
|
150
374
|
const revenue = [];
|
|
151
375
|
const expenses = [];
|
|
152
376
|
for (const row of csv.rows) {
|
|
153
377
|
const desc = descCol >= 0 ? row[descCol] : "Item";
|
|
154
|
-
const amount = Math.abs(
|
|
378
|
+
const amount = Math.abs(parseNum(row[amountCol]));
|
|
155
379
|
const type = typeCol >= 0 ? row[typeCol].toLowerCase() : "";
|
|
156
380
|
if (amount === 0)
|
|
157
381
|
continue;
|
|
@@ -166,3 +390,23 @@ function csvToPnl(csv) {
|
|
|
166
390
|
return null;
|
|
167
391
|
return { revenue, expenses };
|
|
168
392
|
}
|
|
393
|
+
function csvToChart(csv, fileName) {
|
|
394
|
+
if (csv.summary.numericColumns.length === 0)
|
|
395
|
+
return null;
|
|
396
|
+
const h = csv.headers.map(h => h.toLowerCase());
|
|
397
|
+
const labelCol = findCol(h, /name|description|category|month|item|label|employee/);
|
|
398
|
+
const numCol = csv.headers.findIndex(hdr => csv.columnTypes[hdr] === "number" || csv.columnTypes[hdr] === "currency");
|
|
399
|
+
if (numCol === -1)
|
|
400
|
+
return null;
|
|
401
|
+
const labels = labelCol >= 0 ? csv.rows.map(r => r[labelCol]).slice(0, 20) : csv.rows.map((_, i) => `Row ${i + 1}`).slice(0, 20);
|
|
402
|
+
const data = csv.rows.map(r => parseNum(r[numCol])).filter(n => !isNaN(n)).slice(0, 20);
|
|
403
|
+
// Pick chart type: line only for time-series data, bar for everything else
|
|
404
|
+
const isTimeSeries = labelCol >= 0 && csv.columnTypes[csv.headers[labelCol]] === "date";
|
|
405
|
+
const chartType = isTimeSeries ? "line" : "bar";
|
|
406
|
+
return {
|
|
407
|
+
chart_type: chartType,
|
|
408
|
+
title: fileName.replace(/\.[^.]+$/, "").replace(/[_-]/g, " "),
|
|
409
|
+
labels,
|
|
410
|
+
datasets: [{ label: csv.headers[numCol], data }],
|
|
411
|
+
};
|
|
412
|
+
}
|
package/dist/parsers/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File parser registry.
|
|
3
|
-
* Detects file type
|
|
3
|
+
* Detects file type, extracts structured data, and determines the best handler.
|
|
4
4
|
*/
|
|
5
5
|
import { parseCSV, type CSVData } from "./csv.js";
|
|
6
6
|
export interface ParsedFile {
|
|
@@ -10,8 +10,7 @@ export interface ParsedFile {
|
|
|
10
10
|
suggestedActions: string[];
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* Parse file content
|
|
14
|
-
* Returns structured data + suggested actions the bot can take.
|
|
13
|
+
* Parse file content and intelligently detect what kind of financial data it contains.
|
|
15
14
|
*/
|
|
16
15
|
export declare function parseFile(fileName: string, fileType: string, content: string | Buffer): ParsedFile;
|
|
17
16
|
export { parseCSV, type CSVData };
|
package/dist/parsers/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* File parser registry.
|
|
4
|
-
* Detects file type
|
|
4
|
+
* Detects file type, extracts structured data, and determines the best handler.
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.parseCSV = void 0;
|
|
@@ -9,8 +9,7 @@ exports.parseFile = parseFile;
|
|
|
9
9
|
const csv_js_1 = require("./csv.js");
|
|
10
10
|
Object.defineProperty(exports, "parseCSV", { enumerable: true, get: function () { return csv_js_1.parseCSV; } });
|
|
11
11
|
/**
|
|
12
|
-
* Parse file content
|
|
13
|
-
* Returns structured data + suggested actions the bot can take.
|
|
12
|
+
* Parse file content and intelligently detect what kind of financial data it contains.
|
|
14
13
|
*/
|
|
15
14
|
function parseFile(fileName, fileType, content) {
|
|
16
15
|
const lower = fileName.toLowerCase();
|
|
@@ -18,33 +17,16 @@ function parseFile(fileName, fileType, content) {
|
|
|
18
17
|
if (fileType === "csv" || lower.endsWith(".csv")) {
|
|
19
18
|
const text = typeof content === "string" ? content : content.toString("utf-8");
|
|
20
19
|
const data = (0, csv_js_1.parseCSV)(text);
|
|
21
|
-
const suggestedActions =
|
|
22
|
-
// Detect what kind of CSV this is based on headers
|
|
23
|
-
const headersLower = data.headers.map((h) => h.toLowerCase());
|
|
24
|
-
if (headersLower.some((h) => /amount|cost|price|total|expense|debit|credit/.test(h))) {
|
|
25
|
-
suggestedActions.push("expense_report", "chart");
|
|
26
|
-
}
|
|
27
|
-
if (headersLower.some((h) => /revenue|income|sales/.test(h)) && headersLower.some((h) => /expense|cost/.test(h))) {
|
|
28
|
-
suggestedActions.push("pnl");
|
|
29
|
-
}
|
|
30
|
-
if (headersLower.some((h) => /invoice|bill|vendor|supplier/.test(h))) {
|
|
31
|
-
suggestedActions.push("invoice");
|
|
32
|
-
}
|
|
33
|
-
if (headersLower.some((h) => /quantity|stock|inventory|sku/.test(h))) {
|
|
34
|
-
suggestedActions.push("inventory_report");
|
|
35
|
-
}
|
|
36
|
-
if (data.summary.numericColumns.length > 0) {
|
|
37
|
-
suggestedActions.push("chart", "summary");
|
|
38
|
-
}
|
|
20
|
+
const suggestedActions = detectCSVType(data, lower);
|
|
39
21
|
return { type: "csv", fileName, data, suggestedActions };
|
|
40
22
|
}
|
|
41
|
-
// Excel files
|
|
23
|
+
// Excel files
|
|
42
24
|
if (fileType === "excel" || lower.endsWith(".xlsx") || lower.endsWith(".xls")) {
|
|
43
25
|
return {
|
|
44
26
|
type: "excel",
|
|
45
27
|
fileName,
|
|
46
28
|
data: { message: "Excel file detected. Will be parsed by the processing node." },
|
|
47
|
-
suggestedActions:
|
|
29
|
+
suggestedActions: detectFromFileName(lower),
|
|
48
30
|
};
|
|
49
31
|
}
|
|
50
32
|
// PDF files
|
|
@@ -53,47 +35,174 @@ function parseFile(fileName, fileType, content) {
|
|
|
53
35
|
type: "pdf",
|
|
54
36
|
fileName,
|
|
55
37
|
data: { message: "PDF file detected. Will be parsed by the processing node." },
|
|
56
|
-
suggestedActions:
|
|
38
|
+
suggestedActions: detectFromFileName(lower),
|
|
57
39
|
};
|
|
58
40
|
}
|
|
59
41
|
// Text files
|
|
60
42
|
if (fileType === "text" || lower.endsWith(".txt")) {
|
|
61
43
|
const text = typeof content === "string" ? content : content.toString("utf-8");
|
|
62
|
-
return {
|
|
63
|
-
type: "text",
|
|
64
|
-
fileName,
|
|
65
|
-
data: { content: text, length: text.length },
|
|
66
|
-
suggestedActions: ["summary"],
|
|
67
|
-
};
|
|
44
|
+
return { type: "text", fileName, data: { content: text, length: text.length }, suggestedActions: ["summary"] };
|
|
68
45
|
}
|
|
69
46
|
// JSON files
|
|
70
47
|
if (fileType === "json" || lower.endsWith(".json")) {
|
|
71
48
|
const text = typeof content === "string" ? content : content.toString("utf-8");
|
|
72
49
|
try {
|
|
73
50
|
const parsed = JSON.parse(text);
|
|
74
|
-
return {
|
|
75
|
-
type: "json",
|
|
76
|
-
fileName,
|
|
77
|
-
data: { content: parsed, keys: Object.keys(parsed) },
|
|
78
|
-
suggestedActions: ["summary", "chart"],
|
|
79
|
-
};
|
|
51
|
+
return { type: "json", fileName, data: { content: parsed, keys: Object.keys(parsed) }, suggestedActions: ["summary", "chart"] };
|
|
80
52
|
}
|
|
81
53
|
catch {
|
|
82
54
|
return { type: "json", fileName, data: { error: "Invalid JSON" }, suggestedActions: [] };
|
|
83
55
|
}
|
|
84
56
|
}
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
type: "unknown",
|
|
88
|
-
fileName,
|
|
89
|
-
data: { message: `Unrecognized file type: ${fileType}` },
|
|
90
|
-
suggestedActions: [],
|
|
91
|
-
};
|
|
57
|
+
return { type: "unknown", fileName, data: { message: `Unrecognized file type: ${fileType}` }, suggestedActions: [] };
|
|
92
58
|
}
|
|
93
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Intelligently detect the type of CSV data based on headers, column types, and file name.
|
|
61
|
+
* This is the core detection logic — order matters.
|
|
62
|
+
*/
|
|
63
|
+
function detectCSVType(data, fileName) {
|
|
64
|
+
const headers = data.headers.map(h => h.toLowerCase());
|
|
65
|
+
const actions = [];
|
|
66
|
+
// ─── BANK STATEMENT detection ──────────────────────────
|
|
67
|
+
// Key signals: has debit/credit type column, reference numbers, deposit/withdrawal descriptions
|
|
68
|
+
const hasTypeCol = headers.some(h => /^type$/.test(h));
|
|
69
|
+
const hasRefCol = headers.some(h => /reference|ref|check|confirmation/.test(h));
|
|
70
|
+
const hasDebitCreditValues = data.rows.some(r => {
|
|
71
|
+
const typeIdx = headers.findIndex(h => /^type$/.test(h));
|
|
72
|
+
if (typeIdx >= 0) {
|
|
73
|
+
const val = (r[typeIdx] || "").toLowerCase();
|
|
74
|
+
return val === "debit" || val === "credit";
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
});
|
|
78
|
+
const hasDepositDescriptions = data.rows.some(r => {
|
|
79
|
+
const descIdx = headers.findIndex(h => /description/.test(h));
|
|
80
|
+
return descIdx >= 0 && /deposit|withdrawal|transfer|ach|wire|fee|interest/i.test(r[descIdx] || "");
|
|
81
|
+
});
|
|
82
|
+
if ((hasTypeCol && hasDebitCreditValues) || (hasRefCol && hasDepositDescriptions) || /bank|statement|reconcil/i.test(fileName)) {
|
|
83
|
+
actions.push("bank_reconciliation");
|
|
84
|
+
return actions;
|
|
85
|
+
}
|
|
86
|
+
// ─── INVOICE / AR detection ────────────────────────────
|
|
87
|
+
// Key signals: customer column, invoice number, due date, status (paid/unpaid/overdue)
|
|
88
|
+
const hasCustomerCol = headers.some(h => /customer|client|bill.?to/.test(h));
|
|
89
|
+
const hasInvoiceNumCol = headers.some(h => /invoice.?(?:number|no|num|#)|inv.?(?:no|num|#)/.test(h));
|
|
90
|
+
const hasDueDateCol = headers.some(h => /due.?date|payment.?due/.test(h));
|
|
91
|
+
const hasStatusCol = headers.some(h => /status/.test(h));
|
|
92
|
+
const hasStatusValues = data.rows.some(r => {
|
|
93
|
+
const statusIdx = headers.findIndex(h => /status/.test(h));
|
|
94
|
+
return statusIdx >= 0 && /paid|unpaid|overdue|outstanding|pending/i.test(r[statusIdx] || "");
|
|
95
|
+
});
|
|
96
|
+
if ((hasCustomerCol && hasInvoiceNumCol) || (hasInvoiceNumCol && hasDueDateCol) || (hasStatusValues && hasCustomerCol) || /invoice/i.test(fileName)) {
|
|
97
|
+
actions.push("ar_aging");
|
|
98
|
+
return actions;
|
|
99
|
+
}
|
|
100
|
+
// ─── AP / BILLS detection ──────────────────────────────
|
|
101
|
+
const hasVendorCol = headers.some(h => /vendor|supplier|payee/.test(h));
|
|
102
|
+
const hasBillNumCol = headers.some(h => /bill.?(?:number|no|num|#)/.test(h));
|
|
103
|
+
if ((hasVendorCol && hasBillNumCol) || (hasVendorCol && hasDueDateCol) || /bill|payable/i.test(fileName)) {
|
|
104
|
+
actions.push("ap_aging");
|
|
105
|
+
return actions;
|
|
106
|
+
}
|
|
107
|
+
// ─── PAYROLL detection ─────────────────────────────────
|
|
108
|
+
// Key signals: employee name, rate/salary, hours, department, type (salary/hourly)
|
|
109
|
+
const hasEmployeeCol = headers.some(h => /employee|name/.test(h));
|
|
110
|
+
const hasRateCol = headers.some(h => /rate|salary|wage|pay/.test(h));
|
|
111
|
+
const hasHoursCol = headers.some(h => /hours/.test(h));
|
|
112
|
+
const hasTypePayroll = data.rows.some(r => {
|
|
113
|
+
const typeIdx = headers.findIndex(h => /^type$/.test(h));
|
|
114
|
+
return typeIdx >= 0 && /salary|hourly/i.test(r[typeIdx] || "");
|
|
115
|
+
});
|
|
116
|
+
if ((hasEmployeeCol && hasRateCol) || (hasEmployeeCol && hasHoursCol) || hasTypePayroll || /payroll|employee|salary|wage/i.test(fileName)) {
|
|
117
|
+
actions.push("payroll");
|
|
118
|
+
return actions;
|
|
119
|
+
}
|
|
120
|
+
// ─── BUDGET VS ACTUALS detection ───────────────────────
|
|
121
|
+
const hasBudgetCol = headers.some(h => /budget/.test(h));
|
|
122
|
+
const hasActualCol = headers.some(h => /actual/.test(h));
|
|
123
|
+
if (hasBudgetCol && hasActualCol) {
|
|
124
|
+
actions.push("budget_vs_actuals");
|
|
125
|
+
return actions;
|
|
126
|
+
}
|
|
127
|
+
if (/budget/i.test(fileName) && (hasBudgetCol || hasActualCol)) {
|
|
128
|
+
actions.push("budget_vs_actuals");
|
|
129
|
+
return actions;
|
|
130
|
+
}
|
|
131
|
+
// ─── P&L detection ─────────────────────────────────────
|
|
132
|
+
const hasRevenueData = headers.some(h => /revenue|income|sales/.test(h)) || data.rows.some(r => /revenue|income|sales/i.test(r.join(" ")));
|
|
133
|
+
const hasExpenseData = headers.some(h => /expense|cost/.test(h)) || data.rows.some(r => /expense|cost of goods/i.test(r.join(" ")));
|
|
134
|
+
if (hasRevenueData && hasExpenseData) {
|
|
135
|
+
actions.push("pnl");
|
|
136
|
+
return actions;
|
|
137
|
+
}
|
|
138
|
+
if (/p&l|pnl|profit.?loss|income.?statement/i.test(fileName)) {
|
|
139
|
+
actions.push("pnl");
|
|
140
|
+
return actions;
|
|
141
|
+
}
|
|
142
|
+
// ─── SALES TAX detection ───────────────────────────────
|
|
143
|
+
const hasTaxRateCol = headers.some(h => /tax.?rate/.test(h));
|
|
144
|
+
const hasTaxableCol = headers.some(h => /taxable/.test(h));
|
|
145
|
+
const hasStateCol = headers.some(h => /^state$/.test(h));
|
|
146
|
+
if (hasTaxRateCol || (hasTaxableCol && hasStateCol) || /sales.?tax|tax.?report/i.test(fileName)) {
|
|
147
|
+
actions.push("sales_tax");
|
|
148
|
+
return actions;
|
|
149
|
+
}
|
|
150
|
+
// ─── DEPRECIATION / ASSET detection ────────────────────
|
|
151
|
+
const hasUsefulLifeCol = headers.some(h => /useful.?life|lifespan/.test(h));
|
|
152
|
+
const hasSalvageCol = headers.some(h => /salvage|residual/.test(h));
|
|
153
|
+
const hasAcquiredCol = headers.some(h => /acquired|purchase.?date/.test(h));
|
|
154
|
+
if (hasUsefulLifeCol || hasSalvageCol || /deprec|asset|equipment/i.test(fileName)) {
|
|
155
|
+
actions.push("depreciation");
|
|
156
|
+
return actions;
|
|
157
|
+
}
|
|
158
|
+
// ─── DEPARTMENT SPENDING detection ─────────────────────
|
|
159
|
+
const hasDepartmentCol = headers.some(h => /department|dept|team|division|cost.?center/.test(h));
|
|
160
|
+
const hasCategoryCol = headers.some(h => /category|type|class/.test(h));
|
|
161
|
+
const hasAmountCol = headers.some(h => /amount|cost|price|total/.test(h));
|
|
162
|
+
if (hasDepartmentCol && hasAmountCol) {
|
|
163
|
+
actions.push("department_spending");
|
|
164
|
+
return actions;
|
|
165
|
+
}
|
|
166
|
+
// ─── EXPENSE REPORT detection (catch-all for financial data) ──
|
|
167
|
+
if (hasAmountCol && hasCategoryCol) {
|
|
168
|
+
actions.push("expense_report");
|
|
169
|
+
return actions;
|
|
170
|
+
}
|
|
171
|
+
if (hasAmountCol && headers.some(h => /description|desc|memo/.test(h))) {
|
|
172
|
+
actions.push("expense_report");
|
|
173
|
+
return actions;
|
|
174
|
+
}
|
|
175
|
+
if (/expense|spending|receipt/i.test(fileName)) {
|
|
176
|
+
actions.push("expense_report");
|
|
177
|
+
return actions;
|
|
178
|
+
}
|
|
179
|
+
// ─── GENERIC fallback ─────────────────────────────────
|
|
180
|
+
if (data.summary.numericColumns.length > 0) {
|
|
181
|
+
actions.push("chart");
|
|
182
|
+
}
|
|
183
|
+
return actions;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Detect file type from file name alone (for PDFs, Excel, etc. where we can't read headers)
|
|
187
|
+
*/
|
|
188
|
+
function detectFromFileName(fileName) {
|
|
94
189
|
if (/invoice/i.test(fileName))
|
|
95
|
-
return ["
|
|
96
|
-
if (/
|
|
190
|
+
return ["ar_aging"];
|
|
191
|
+
if (/bill|payable/i.test(fileName))
|
|
192
|
+
return ["ap_aging"];
|
|
193
|
+
if (/bank|statement|reconcil/i.test(fileName))
|
|
194
|
+
return ["bank_reconciliation"];
|
|
195
|
+
if (/payroll|employee|salary/i.test(fileName))
|
|
196
|
+
return ["payroll"];
|
|
197
|
+
if (/budget/i.test(fileName))
|
|
198
|
+
return ["budget_vs_actuals"];
|
|
199
|
+
if (/p&l|pnl|profit/i.test(fileName))
|
|
200
|
+
return ["pnl"];
|
|
201
|
+
if (/tax/i.test(fileName))
|
|
202
|
+
return ["sales_tax"];
|
|
203
|
+
if (/deprec|asset/i.test(fileName))
|
|
204
|
+
return ["depreciation"];
|
|
205
|
+
if (/expense|receipt|spending/i.test(fileName))
|
|
97
206
|
return ["expense_report"];
|
|
98
207
|
if (/report|statement/i.test(fileName))
|
|
99
208
|
return ["summary", "chart"];
|