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.
@@ -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 what to do,
5
- * then routes to the appropriate specialized handler.
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 invoice_js_1 = require("./invoice.js");
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 what to do,
12
- * then routes to the appropriate specialized handler.
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
- // If there are suggested actions, auto-execute the best one
23
- if (parsed.suggestedActions.length > 0) {
24
- const bestAction = parsed.suggestedActions[0];
25
- // Build input from parsed data
26
- if (bestAction === "expense_report" && parsed.type === "csv") {
27
- const csvData = parsed.data;
28
- const expenses = csvToExpenses(csvData);
29
- if (expenses.length > 0) {
30
- const result = await (0, expense_js_1.handleExpenseReport)({
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
- if (bestAction === "chart" && parsed.type === "csv") {
43
- const csvData = parsed.data;
44
- const chartInput = csvToChart(csvData, fileName);
45
- if (chartInput) {
46
- const result = await (0, chart_js_1.handleChart)({
47
- type: "chart",
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
- if (bestAction === "pnl" && parsed.type === "csv") {
59
- const csvData = parsed.data;
60
- const pnlInput = csvToPnl(csvData);
61
- if (pnlInput) {
62
- const result = await (0, pnl_js_1.handlePnl)({
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
- // Default: return parsed summary
88
- const durationMs = Date.now() - start;
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}" (${fileType}).\n\n${JSON.stringify(parsed.data, null, 2)}`,
172
+ text: `Parsed "${fileName}" ${csvData.rowCount} rows, ${csvData.headers.length} columns.\n\nHeaders: ${csvData.headers.join(", ")}`,
91
173
  parsed_file: parsed,
92
- suggested_actions: parsed.suggestedActions,
93
- duration_ms: durationMs,
174
+ duration_ms: Date.now() - start,
94
175
  };
95
176
  }
96
- /**
97
- * Convert CSV data into expense objects for the expense handler.
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 headersLower = csv.headers.map((h) => h.toLowerCase());
101
- // Find relevant columns
102
- const descCol = headersLower.findIndex((h) => /description|name|item|memo/.test(h));
103
- const amountCol = headersLower.findIndex((h) => /amount|cost|price|total|debit/.test(h));
104
- const categoryCol = headersLower.findIndex((h) => /category|type|class|department/.test(h));
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((row) => ({
109
- description: descCol >= 0 ? row[descCol] : "Item",
110
- amount: parseFloat((row[amountCol] || "0").replace(/[$,]/g, "")) || 0,
111
- category: categoryCol >= 0 ? row[categoryCol] : "Uncategorized",
112
- date: dateCol >= 0 ? row[dateCol] : undefined,
113
- })).filter((e) => e.amount > 0);
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
- * Convert CSV data into chart parameters.
117
- */
118
- function csvToChart(csv, fileName) {
119
- if (csv.summary.numericColumns.length === 0)
120
- return null;
121
- const headersLower = csv.headers.map((h) => h.toLowerCase());
122
- const labelCol = headersLower.findIndex((h) => /name|description|category|date|month|item|label/.test(h));
123
- const numCol = csv.headers.findIndex((h) => csv.columnTypes[h] === "number" || csv.columnTypes[h] === "currency");
124
- if (numCol === -1)
125
- return null;
126
- const labels = labelCol >= 0
127
- ? csv.rows.map((r) => r[labelCol]).slice(0, 20)
128
- : csv.rows.map((_, i) => `Row ${i + 1}`).slice(0, 20);
129
- const data = csv.rows
130
- .map((r) => parseFloat((r[numCol] || "0").replace(/[$,]/g, "")))
131
- .filter((n) => !isNaN(n))
132
- .slice(0, 20);
133
- return {
134
- chart_type: data.length > 8 ? "line" : "bar",
135
- title: fileName.replace(/\.[^.]+$/, "").replace(/[_-]/g, " "),
136
- labels,
137
- datasets: [{ label: csv.headers[numCol], data }],
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 headersLower = csv.headers.map((h) => h.toLowerCase());
145
- const descCol = headersLower.findIndex((h) => /description|name|item/.test(h));
146
- const amountCol = headersLower.findIndex((h) => /amount|total/.test(h));
147
- const typeCol = headersLower.findIndex((h) => /type|category/.test(h));
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(parseFloat((row[amountCol] || "0").replace(/[$,]/g, "")));
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
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * File parser registry.
3
- * Detects file type and extracts structured data.
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 based on its type.
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 };
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /**
3
3
  * File parser registry.
4
- * Detects file type and extracts structured data.
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 based on its type.
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 (basic detection — full parsing requires xlsx library on the node)
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: ["chart", "expense_report", "summary"],
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: detectPDFActions(lower),
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
- // Unknown
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
- function detectPDFActions(fileName) {
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 ["invoice", "expense_report"];
96
- if (/receipt/i.test(fileName))
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"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuronix-node",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Neuronix GPU Provider Node — earn by contributing compute to the Neuronix network",
5
5
  "main": "dist/index.js",
6
6
  "bin": {