neuronix-node 0.7.1 → 0.8.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.
|
@@ -8,6 +8,11 @@ async function handleBankReconciliation(task) {
|
|
|
8
8
|
const bookTxns = input.book_transactions || [];
|
|
9
9
|
const bankBal = input.bank_balance || 0;
|
|
10
10
|
const bookBal = input.book_balance || 0;
|
|
11
|
+
// If we only have bank transactions and no book transactions,
|
|
12
|
+
// generate a transaction summary instead of a reconciliation
|
|
13
|
+
if (bankTxns.length > 0 && bookTxns.length === 0) {
|
|
14
|
+
return generateBankSummary(bankTxns, start);
|
|
15
|
+
}
|
|
11
16
|
// Match transactions by amount and approximate date
|
|
12
17
|
const matched = [];
|
|
13
18
|
const unmatchedBank = [];
|
|
@@ -91,6 +96,86 @@ async function handleBankReconciliation(task) {
|
|
|
91
96
|
duration_ms: Date.now() - start,
|
|
92
97
|
};
|
|
93
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* When only a bank statement is provided (no book data), generate a useful
|
|
101
|
+
* transaction summary instead of a broken reconciliation.
|
|
102
|
+
*/
|
|
103
|
+
function generateBankSummary(txns, start) {
|
|
104
|
+
const deposits = txns.filter(t => t.type === "credit");
|
|
105
|
+
const withdrawals = txns.filter(t => t.type === "debit");
|
|
106
|
+
const totalDeposits = round(deposits.reduce((s, t) => s + t.amount, 0));
|
|
107
|
+
const totalWithdrawals = round(withdrawals.reduce((s, t) => s + t.amount, 0));
|
|
108
|
+
const netCashFlow = round(totalDeposits - totalWithdrawals);
|
|
109
|
+
// Categorize by description patterns
|
|
110
|
+
const categories = {};
|
|
111
|
+
for (const t of txns) {
|
|
112
|
+
const cat = categorizeTransaction(t.description);
|
|
113
|
+
if (!categories[cat])
|
|
114
|
+
categories[cat] = { total: 0, count: 0, type: t.type };
|
|
115
|
+
categories[cat].total += t.amount;
|
|
116
|
+
categories[cat].count += 1;
|
|
117
|
+
}
|
|
118
|
+
const sortedCats = Object.entries(categories).sort((a, b) => b[1].total - a[1].total);
|
|
119
|
+
const csv = [
|
|
120
|
+
"BANK STATEMENT SUMMARY",
|
|
121
|
+
`Date: ${new Date().toISOString().split("T")[0]}`,
|
|
122
|
+
`Transactions: ${txns.length}`,
|
|
123
|
+
"",
|
|
124
|
+
"CASH FLOW SUMMARY",
|
|
125
|
+
`Total Deposits (Credits),${fmt(totalDeposits)},${deposits.length} transactions`,
|
|
126
|
+
`Total Withdrawals (Debits),${fmt(totalWithdrawals)},${withdrawals.length} transactions`,
|
|
127
|
+
`Net Cash Flow,${fmt(netCashFlow)},${netCashFlow >= 0 ? "Positive" : "Negative"}`,
|
|
128
|
+
"",
|
|
129
|
+
"BY CATEGORY",
|
|
130
|
+
"Category,Total,Transactions,Type",
|
|
131
|
+
...sortedCats.map(([cat, data]) => `${esc(cat)},${fmt(data.total)},${data.count},${data.type}`),
|
|
132
|
+
"",
|
|
133
|
+
"DEPOSITS",
|
|
134
|
+
"Date,Description,Amount,Reference",
|
|
135
|
+
...deposits.sort((a, b) => b.amount - a.amount).map(t => `${t.date},${esc(t.description)},${fmt(t.amount)},${t.reference || ""}`),
|
|
136
|
+
"",
|
|
137
|
+
"WITHDRAWALS",
|
|
138
|
+
"Date,Description,Amount,Reference",
|
|
139
|
+
...withdrawals.sort((a, b) => b.amount - a.amount).map(t => `${t.date},${esc(t.description)},${fmt(t.amount)},${t.reference || ""}`),
|
|
140
|
+
"",
|
|
141
|
+
"NOTE",
|
|
142
|
+
"This is a bank statement summary. To run a full reconciliation upload both your bank statement AND your book/ledger file.",
|
|
143
|
+
];
|
|
144
|
+
return {
|
|
145
|
+
text: `Bank Statement Summary: ${txns.length} transactions | Deposits: ${fmt(totalDeposits)} | Withdrawals: ${fmt(totalWithdrawals)} | Net: ${fmt(netCashFlow)}`,
|
|
146
|
+
output_csv: csv.join("\n"),
|
|
147
|
+
summary: { total_deposits: totalDeposits, total_withdrawals: totalWithdrawals, net_cash_flow: netCashFlow, transaction_count: txns.length },
|
|
148
|
+
duration_ms: Date.now() - start,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function categorizeTransaction(desc) {
|
|
152
|
+
const lower = desc.toLowerCase();
|
|
153
|
+
if (/payroll|adp|gusto/.test(lower))
|
|
154
|
+
return "Payroll";
|
|
155
|
+
if (/deposit|payment|wire|transfer/i.test(lower) && /customer|inv-|client/.test(lower))
|
|
156
|
+
return "Customer Payments";
|
|
157
|
+
if (/rent|landlord/.test(lower))
|
|
158
|
+
return "Rent";
|
|
159
|
+
if (/insurance/.test(lower))
|
|
160
|
+
return "Insurance";
|
|
161
|
+
if (/aws|hosting|cloud|github|slack|zoom|google|datadog|sentry/.test(lower))
|
|
162
|
+
return "Software & Cloud";
|
|
163
|
+
if (/electric|pge|water|comcast|internet|phone/.test(lower))
|
|
164
|
+
return "Utilities";
|
|
165
|
+
if (/marketing|agency/.test(lower))
|
|
166
|
+
return "Marketing";
|
|
167
|
+
if (/interest/.test(lower))
|
|
168
|
+
return "Interest";
|
|
169
|
+
if (/fee|charge/.test(lower))
|
|
170
|
+
return "Bank Fees";
|
|
171
|
+
if (/uber|travel|lunch|dinner/.test(lower))
|
|
172
|
+
return "Travel & Entertainment";
|
|
173
|
+
if (/credit card|chase|amex/.test(lower))
|
|
174
|
+
return "Credit Card Payments";
|
|
175
|
+
if (/office|supplies|depot/.test(lower))
|
|
176
|
+
return "Office Supplies";
|
|
177
|
+
return "Other";
|
|
178
|
+
}
|
|
94
179
|
function round(n) { return Math.round(n * 100) / 100; }
|
|
95
180
|
function fmt(n) { return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }
|
|
96
181
|
function esc(v) { return v.includes(",") ? `"${v}"` : v; }
|
|
@@ -303,15 +303,56 @@ function csvToPayroll(csv) {
|
|
|
303
303
|
const rateCol = findCol(h, /rate|salary|wage|pay/);
|
|
304
304
|
const hoursCol = findCol(h, /hours/);
|
|
305
305
|
const deptCol = findCol(h, /department|dept/);
|
|
306
|
+
const healthInsCol = findCol(h, /health.?ins|medical|health/);
|
|
307
|
+
const k401Col = findCol(h, /401k|retirement/);
|
|
308
|
+
const otherDeductCol = findCol(h, /other.?deduct|deduction/);
|
|
306
309
|
if (nameCol === -1 || rateCol === -1)
|
|
307
310
|
return [];
|
|
308
311
|
return csv.rows.map(r => {
|
|
309
312
|
const type = typeCol >= 0 ? (r[typeCol]?.toLowerCase().includes("hourly") ? "hourly" : "salary") : "salary";
|
|
313
|
+
// Build deductions from columns
|
|
314
|
+
const deductions = [];
|
|
315
|
+
if (healthInsCol >= 0 && r[healthInsCol]) {
|
|
316
|
+
const amt = parseNum(r[healthInsCol]);
|
|
317
|
+
if (amt > 0)
|
|
318
|
+
deductions.push({ name: "Health Insurance", amount: amt });
|
|
319
|
+
}
|
|
320
|
+
if (k401Col >= 0 && r[k401Col]) {
|
|
321
|
+
const val = r[k401Col].trim();
|
|
322
|
+
// Could be a percentage like "6" or a dollar amount
|
|
323
|
+
if (val.includes("%") || (parseFloat(val) > 0 && parseFloat(val) <= 100 && !val.includes("$"))) {
|
|
324
|
+
// It's a percentage — will be calculated by the payroll handler based on gross
|
|
325
|
+
const pct = parseFloat(val);
|
|
326
|
+
const rate = parseNum(r[rateCol]);
|
|
327
|
+
const gross = type === "salary" ? rate / 26 : rate * (hoursCol >= 0 ? parseNum(r[hoursCol]) : 80);
|
|
328
|
+
const amt = Math.round(gross * (pct / 100) * 100) / 100;
|
|
329
|
+
if (amt > 0)
|
|
330
|
+
deductions.push({ name: `401k (${pct}%)`, amount: amt });
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
const amt = parseNum(val);
|
|
334
|
+
if (amt > 0)
|
|
335
|
+
deductions.push({ name: "401k", amount: amt });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (otherDeductCol >= 0 && r[otherDeductCol]) {
|
|
339
|
+
// Parse "HSA:150" or "HSA:150,Union:50" format
|
|
340
|
+
const parts = r[otherDeductCol].split(",");
|
|
341
|
+
for (const part of parts) {
|
|
342
|
+
const [name, amtStr] = part.split(":");
|
|
343
|
+
if (name && amtStr) {
|
|
344
|
+
const amt = parseFloat(amtStr.trim());
|
|
345
|
+
if (amt > 0)
|
|
346
|
+
deductions.push({ name: name.trim(), amount: amt });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
310
350
|
return {
|
|
311
351
|
name: r[nameCol] || "Employee",
|
|
312
352
|
type: type,
|
|
313
353
|
rate: parseNum(r[rateCol]),
|
|
314
354
|
hours: hoursCol >= 0 && r[hoursCol] ? parseNum(r[hoursCol]) : undefined,
|
|
355
|
+
deductions: deductions.length > 0 ? deductions : undefined,
|
|
315
356
|
};
|
|
316
357
|
}).filter(e => e.rate > 0);
|
|
317
358
|
}
|
package/dist/index.js
CHANGED
|
@@ -236,7 +236,11 @@ async function main() {
|
|
|
236
236
|
const resources = await (0, resource_limiter_js_1.checkResources)();
|
|
237
237
|
if (!resources.allowed) {
|
|
238
238
|
log(`Task deferred: ${resources.reason}`);
|
|
239
|
-
//
|
|
239
|
+
// Put task back to pending so another node can pick it up
|
|
240
|
+
try {
|
|
241
|
+
await (0, api_js_1.completeTask)(config, task.id, "failed", { error: "Node resource limits exceeded, retrying on another node" }, 0);
|
|
242
|
+
}
|
|
243
|
+
catch { }
|
|
240
244
|
continue;
|
|
241
245
|
}
|
|
242
246
|
// ── Security: Log task receipt ──
|