neuronix-node 0.2.0 → 0.5.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.
@@ -0,0 +1,336 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleChat = handleChat;
4
+ const inference_js_1 = require("../inference.js");
5
+ // Import all action handlers
6
+ const chart_js_1 = require("./chart.js");
7
+ const invoice_js_1 = require("./invoice.js");
8
+ const expense_js_1 = require("./expense.js");
9
+ const pnl_js_1 = require("./pnl.js");
10
+ const ar_aging_js_1 = require("./ar-aging.js");
11
+ const ap_aging_js_1 = require("./ap-aging.js");
12
+ const bank_reconciliation_js_1 = require("./bank-reconciliation.js");
13
+ const budget_vs_actuals_js_1 = require("./budget-vs-actuals.js");
14
+ const payroll_js_1 = require("./payroll.js");
15
+ const sales_tax_js_1 = require("./sales-tax.js");
16
+ const depreciation_js_1 = require("./depreciation.js");
17
+ const cash_flow_js_1 = require("./cash-flow.js");
18
+ const department_spending_js_1 = require("./department-spending.js");
19
+ const variance_analysis_js_1 = require("./variance-analysis.js");
20
+ const w2_1099_js_1 = require("./w2-1099.js");
21
+ // Intent detection patterns — maps user language to handler actions
22
+ const INTENT_PATTERNS = [
23
+ // Expense report
24
+ { pattern: /\b(expense report|expense summary|categorize expenses|expense breakdown|spending report)\b/i, action: "expense_report", handler: "expense_report", description: "generating an expense report" },
25
+ // P&L
26
+ { pattern: /\b(p&l|profit.?(?:and|&)?.?loss|income statement|net income|profit margin)\b/i, action: "pnl", handler: "pnl", description: "generating a Profit & Loss statement" },
27
+ // Invoice
28
+ { pattern: /\b(create invoice|generate invoice|make (?:an )?invoice|send invoice|bill (?:the )?client)\b/i, action: "invoice", handler: "invoice", description: "creating an invoice" },
29
+ // Chart
30
+ { pattern: /\b(make (?:a |me )?chart|create (?:a )?(?:chart|graph|plot)|bar chart|pie chart|line chart|visualize|visualization)\b/i, action: "chart", handler: "chart", description: "creating a chart" },
31
+ // AR Aging
32
+ { pattern: /\b(ar aging|accounts receivable|who owes|outstanding invoices|overdue invoices|collections|receivables aging)\b/i, action: "ar_aging", handler: "ar_aging", description: "generating an Accounts Receivable aging report" },
33
+ // AP Aging
34
+ { pattern: /\b(ap aging|accounts payable|what (?:do )?we owe|outstanding bills|overdue bills|vendor payments|payables aging)\b/i, action: "ap_aging", handler: "ap_aging", description: "generating an Accounts Payable aging report" },
35
+ // Bank Reconciliation
36
+ { pattern: /\b(bank rec|reconcil|match.?(?:bank|transactions)|bank statement|reconcile (?:the )?bank)\b/i, action: "bank_reconciliation", handler: "bank_reconciliation", description: "running a bank reconciliation" },
37
+ // Budget vs Actuals
38
+ { pattern: /\b(budget vs|budget.?actual|over budget|under budget|budget comparison|budget review|spending vs budget)\b/i, action: "budget_vs_actuals", handler: "budget_vs_actuals", description: "comparing budget vs actuals" },
39
+ // Payroll
40
+ { pattern: /\b(run payroll|payroll calc|process payroll|payroll register|employee pay|calculate payroll|pay (?:the )?employees)\b/i, action: "payroll", handler: "payroll", description: "running payroll calculations" },
41
+ // Sales Tax
42
+ { pattern: /\b(sales tax|tax collected|tax filing|tax report|tax liability|state tax)\b/i, action: "sales_tax", handler: "sales_tax", description: "calculating sales tax" },
43
+ // Depreciation
44
+ { pattern: /\b(depreciation|fixed asset|book value|asset schedule|depreciate)\b/i, action: "depreciation", handler: "depreciation", description: "calculating depreciation" },
45
+ // Cash Flow
46
+ { pattern: /\b(cash flow|cash position|cash statement|operating cash|where.?(?:is|did) (?:the |our )?(?:cash|money))\b/i, action: "cash_flow", handler: "cash_flow", description: "generating a cash flow statement" },
47
+ // Department Spending
48
+ { pattern: /\b(department spending|spending by department|departmental|team spending|which department|cost center)\b/i, action: "department_spending", handler: "department_spending", description: "generating a department spending report" },
49
+ // Variance Analysis
50
+ { pattern: /\b(variance|significant.?difference|budget.?deviation|why.?(?:is|are|did).?(?:we|it).?(?:over|under|different))\b/i, action: "variance_analysis", handler: "variance_analysis", description: "running a variance analysis" },
51
+ // W-2/1099
52
+ { pattern: /\b(w-?2|1099|year.?end.?tax|tax.?form|issue.?w2|file.?1099|annual.?tax)\b/i, action: "w2_1099", handler: "w2_1099", description: "compiling W-2 and 1099 data" },
53
+ ];
54
+ // Map action names to handler functions
55
+ const ACTION_HANDLERS = {
56
+ expense_report: expense_js_1.handleExpenseReport,
57
+ pnl: pnl_js_1.handlePnl,
58
+ invoice: invoice_js_1.handleInvoice,
59
+ chart: chart_js_1.handleChart,
60
+ ar_aging: ar_aging_js_1.handleARAging,
61
+ ap_aging: ap_aging_js_1.handleAPAging,
62
+ bank_reconciliation: bank_reconciliation_js_1.handleBankReconciliation,
63
+ budget_vs_actuals: budget_vs_actuals_js_1.handleBudgetVsActuals,
64
+ payroll: payroll_js_1.handlePayroll,
65
+ sales_tax: sales_tax_js_1.handleSalesTax,
66
+ depreciation: depreciation_js_1.handleDepreciation,
67
+ cash_flow: cash_flow_js_1.handleCashFlow,
68
+ department_spending: department_spending_js_1.handleDepartmentSpending,
69
+ variance_analysis: variance_analysis_js_1.handleVarianceAnalysis,
70
+ w2_1099: w2_1099_js_1.handleW21099,
71
+ };
72
+ /**
73
+ * Detect what the user wants to do from their message.
74
+ */
75
+ function detectIntent(messages) {
76
+ // Check the last user message
77
+ const lastUser = [...messages].reverse().find(m => m.role === "user");
78
+ if (!lastUser)
79
+ return null;
80
+ const text = lastUser.content;
81
+ for (const intent of INTENT_PATTERNS) {
82
+ if (intent.pattern.test(text)) {
83
+ return { action: intent.action, handler: intent.handler, description: intent.description };
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ /**
89
+ * Chat handler — processes conversational messages AND triggers actions.
90
+ */
91
+ async function handleChat(task) {
92
+ const start = Date.now();
93
+ const input = task.input_payload;
94
+ const messages = input.messages || [];
95
+ // Step 1: Detect if the user wants an action
96
+ const intent = detectIntent(messages);
97
+ if (intent) {
98
+ // User wants something done — run the handler
99
+ const handler = ACTION_HANDLERS[intent.action];
100
+ if (handler) {
101
+ try {
102
+ const actionResult = await handler({
103
+ type: intent.action,
104
+ input_payload: task.input_payload,
105
+ });
106
+ // Build a natural response that includes the action result
107
+ const responseText = buildActionResponse(intent, actionResult);
108
+ return {
109
+ text: responseText,
110
+ content: responseText,
111
+ action_taken: intent.action,
112
+ action_description: intent.description,
113
+ action_result: actionResult,
114
+ // Pass through output_csv so it gets saved as a file
115
+ output_csv: actionResult.output_csv,
116
+ image_base64: actionResult.image_base64,
117
+ invoice_html: actionResult.invoice_html,
118
+ conversation_id: input.conversation_id,
119
+ deployment_id: input.deployment_id,
120
+ duration_ms: Date.now() - start,
121
+ };
122
+ }
123
+ catch (err) {
124
+ const durationMs = Date.now() - start;
125
+ return {
126
+ text: `I tried ${intent.description} but ran into an error: ${err}. Could you check the data and try again?`,
127
+ content: `I tried ${intent.description} but ran into an error: ${err}. Could you check the data and try again?`,
128
+ action_taken: intent.action,
129
+ action_error: String(err),
130
+ conversation_id: input.conversation_id,
131
+ deployment_id: input.deployment_id,
132
+ duration_ms: durationMs,
133
+ };
134
+ }
135
+ }
136
+ }
137
+ // Step 2: No action detected — regular conversation
138
+ // Try LLM for conversational response
139
+ const modelIds = ["llama3-8b", "mistral-7b", "phi-2", "tinyllama-1.1b"];
140
+ const loadedModel = modelIds.find((id) => (0, inference_js_1.isModelLoaded)(id));
141
+ if (!loadedModel) {
142
+ // No model loaded — provide a helpful static response
143
+ const helpText = generateHelpResponse(messages);
144
+ return {
145
+ text: helpText,
146
+ content: helpText,
147
+ conversation_id: input.conversation_id,
148
+ deployment_id: input.deployment_id,
149
+ duration_ms: Date.now() - start,
150
+ };
151
+ }
152
+ try {
153
+ const prompt = formatConversation(messages);
154
+ const { text, durationMs } = await (0, inference_js_1.runInference)(loadedModel, prompt, input.max_tokens || 512);
155
+ const cleanedText = cleanResponse(text);
156
+ // Check for preference updates
157
+ const memoryUpdates = detectPreferences(messages);
158
+ let finalText = cleanedText;
159
+ if (memoryUpdates.length > 0) {
160
+ finalText += "\n" + memoryUpdates.map(m => `[MEMORY_UPDATE]${m.key}: ${m.value}[/MEMORY_UPDATE]`).join("\n");
161
+ }
162
+ return {
163
+ text: finalText,
164
+ content: finalText,
165
+ conversation_id: input.conversation_id,
166
+ deployment_id: input.deployment_id,
167
+ model: loadedModel,
168
+ duration_ms: durationMs,
169
+ };
170
+ }
171
+ catch {
172
+ const helpText = generateHelpResponse(messages);
173
+ return {
174
+ text: helpText,
175
+ content: helpText,
176
+ conversation_id: input.conversation_id,
177
+ deployment_id: input.deployment_id,
178
+ duration_ms: Date.now() - start,
179
+ };
180
+ }
181
+ }
182
+ /**
183
+ * Build a natural language response after running an action.
184
+ */
185
+ function buildActionResponse(intent, result) {
186
+ const summary = result.summary;
187
+ const parts = [];
188
+ parts.push(`Done — I've finished ${intent.description}.`);
189
+ // Add key stats from the result
190
+ switch (intent.action) {
191
+ case "expense_report":
192
+ if (summary)
193
+ parts.push(`\nTotal expenses: $${summary.total?.toLocaleString() || "N/A"} across ${summary.expense_count || 0} items in ${summary.category_count || 0} categories.`);
194
+ break;
195
+ case "pnl":
196
+ if (summary)
197
+ parts.push(`\nRevenue: $${summary.revenue_total?.toLocaleString() || "0"} | Expenses: $${summary.expense_total?.toLocaleString() || "0"} | Net Income: $${summary.net_income?.toLocaleString() || "0"} (${summary.margin_pct || 0}% margin)`);
198
+ break;
199
+ case "invoice":
200
+ if (result.total)
201
+ parts.push(`\nInvoice ${result.invoice_number || ""} for $${result.total?.toLocaleString() || "0"}, due ${result.due_date || "in 30 days"}.`);
202
+ break;
203
+ case "ar_aging":
204
+ if (summary)
205
+ parts.push(`\nTotal outstanding: $${summary.total_outstanding?.toLocaleString() || "0"} across ${summary.customer_count || 0} customers.`);
206
+ break;
207
+ case "ap_aging":
208
+ if (summary)
209
+ parts.push(`\nTotal owed: $${summary.total_owed?.toLocaleString() || "0"} across ${summary.vendor_count || 0} vendors.`);
210
+ break;
211
+ case "bank_reconciliation":
212
+ if (summary)
213
+ parts.push(`\n${summary.reconciled ? "The accounts are reconciled." : `There's a difference of $${summary.difference?.toLocaleString() || "0"} that needs review.`} Matched ${summary.matched_count || 0} transactions.`);
214
+ break;
215
+ case "budget_vs_actuals":
216
+ if (summary)
217
+ parts.push(`\nBudget: $${summary.total_budget?.toLocaleString() || "0"} | Actual: $${summary.total_actual?.toLocaleString() || "0"} | Variance: ${summary.total_variance >= 0 ? "+" : ""}$${summary.total_variance?.toLocaleString() || "0"} (${summary.total_variance_pct || 0}%)`);
218
+ break;
219
+ case "payroll":
220
+ if (summary)
221
+ parts.push(`\n${summary.employee_count || 0} employees | Gross: $${summary.total_gross?.toLocaleString() || "0"} | Net: $${summary.total_net?.toLocaleString() || "0"} | Employer cost: $${summary.employer_cost?.toLocaleString() || "0"}`);
222
+ break;
223
+ case "sales_tax":
224
+ if (summary)
225
+ parts.push(`\nTax collected: $${summary.total_tax?.toLocaleString() || "0"} on $${summary.total_taxable?.toLocaleString() || "0"} taxable sales across ${summary.jurisdictions || 0} jurisdictions.`);
226
+ break;
227
+ case "depreciation":
228
+ if (summary)
229
+ parts.push(`\n${summary.asset_count || 0} assets | Book value: $${summary.total_book_value?.toLocaleString() || "0"} | This year's depreciation: $${summary.current_year?.toLocaleString() || "0"}`);
230
+ break;
231
+ case "cash_flow":
232
+ if (summary)
233
+ parts.push(`\nOperating: $${summary.net_operating?.toLocaleString() || "0"} | Investing: $${summary.net_investing?.toLocaleString() || "0"} | Financing: $${summary.net_financing?.toLocaleString() || "0"} | Ending balance: $${summary.ending_balance?.toLocaleString() || "0"}`);
234
+ break;
235
+ case "department_spending":
236
+ if (summary)
237
+ parts.push(`\n${summary.department_count || 0} departments | Total: $${summary.grand_total?.toLocaleString() || "0"}`);
238
+ break;
239
+ case "variance_analysis":
240
+ if (summary)
241
+ parts.push(`\n${summary.significant_count || 0} significant variances found. ${summary.favorable_count || 0} favorable, ${summary.unfavorable_count || 0} unfavorable.`);
242
+ break;
243
+ case "w2_1099":
244
+ if (summary)
245
+ parts.push(`\n${summary.w2_count || 0} W-2s and ${summary.contractor_count || 0} 1099s to issue. Filing deadline: ${summary.filing_deadline || "January 31"}.`);
246
+ break;
247
+ case "chart":
248
+ parts.push(`\nThe chart has been generated.`);
249
+ break;
250
+ }
251
+ if (result.output_csv) {
252
+ parts.push("\nThe report has been saved to your outputs folder. You can download it from the Files tab.");
253
+ }
254
+ parts.push("\nAnything else you need?");
255
+ return parts.join("");
256
+ }
257
+ /**
258
+ * Generate a helpful response when no LLM is available.
259
+ */
260
+ function generateHelpResponse(messages) {
261
+ const lastUser = [...messages].reverse().find(m => m.role === "user");
262
+ const userText = lastUser?.content.toLowerCase() || "";
263
+ if (/help|what can you do|capabilities/.test(userText)) {
264
+ return `Here's what I can do for you. Just ask in plain English:
265
+
266
+ • "Run my expense report" — categorize and summarize expenses
267
+ • "Generate a P&L" — Profit & Loss statement
268
+ • "Create an invoice" — generate a formatted invoice
269
+ • "Run payroll" — calculate gross, taxes, deductions, net pay
270
+ • "Show me AR aging" — who owes money and how overdue
271
+ • "Show me AP aging" — what we owe vendors
272
+ • "Reconcile the bank" — match bank vs book transactions
273
+ • "Budget vs actuals" — compare spending to budget
274
+ • "Calculate sales tax" — multi-state tax tracking
275
+ • "Depreciation schedule" — fixed asset depreciation
276
+ • "Cash flow statement" — operating/investing/financing
277
+ • "Department spending" — spending by department
278
+ • "Variance analysis" — find significant budget variances
279
+ • "W-2 and 1099 prep" — year-end tax form compilation
280
+ • "Make a chart" — visualize any data
281
+
282
+ Just tell me what you need!`;
283
+ }
284
+ if (/hi|hello|hey/.test(userText)) {
285
+ return "Hi! I'm your accounting bot. I can run reports, calculate payroll, reconcile accounts, and more. Just tell me what you need — for example, \"run my expense report\" or \"show me the P&L.\"";
286
+ }
287
+ return "I can help with that. Try asking me to run a specific report like \"expense report\", \"P&L\", \"payroll\", or \"AR aging\". Type \"help\" to see everything I can do.";
288
+ }
289
+ function formatConversation(messages) {
290
+ const parts = [];
291
+ for (const msg of messages) {
292
+ if (msg.role === "system")
293
+ parts.push(`Instructions: ${msg.content}\n`);
294
+ else if (msg.role === "user")
295
+ parts.push(`User: ${msg.content}`);
296
+ else if (msg.role === "assistant")
297
+ parts.push(`Assistant: ${msg.content}`);
298
+ }
299
+ parts.push("Assistant:");
300
+ return parts.join("\n");
301
+ }
302
+ function cleanResponse(text) {
303
+ let cleaned = text.trim();
304
+ const userIdx = cleaned.indexOf("\nUser:");
305
+ if (userIdx > 0)
306
+ cleaned = cleaned.slice(0, userIdx).trim();
307
+ if (cleaned.startsWith("Assistant:"))
308
+ cleaned = cleaned.slice(10).trim();
309
+ if (cleaned.length > 2000)
310
+ cleaned = cleaned.slice(0, 2000) + "...";
311
+ return cleaned || "I understand. How can I help you?";
312
+ }
313
+ function detectPreferences(messages) {
314
+ const updates = [];
315
+ const lastUser = messages.filter(m => m.role === "user").pop();
316
+ if (!lastUser)
317
+ return updates;
318
+ const lower = lastUser.content.toLowerCase();
319
+ if (/always sort|sort by|order by/.test(lower)) {
320
+ const match = lower.match(/sort (?:by|expenses by) (\w+)/);
321
+ if (match)
322
+ updates.push({ key: "sort_preference", value: match[1] });
323
+ }
324
+ if (/always include|i want|i prefer|i like/.test(lower)) {
325
+ if (/department/.test(lower))
326
+ updates.push({ key: "include_departments", value: "true" });
327
+ if (/chart/.test(lower))
328
+ updates.push({ key: "include_charts", value: "true" });
329
+ }
330
+ if (/my company|our company|we are|i work at/.test(lower)) {
331
+ const match = lower.match(/(?:company is|work at|we are) ([^,.]+)/);
332
+ if (match)
333
+ updates.push({ key: "company_name", value: match[1].trim() });
334
+ }
335
+ return updates;
336
+ }
@@ -0,0 +1,2 @@
1
+ import type { TaskInput, TaskOutput } from "./index.js";
2
+ export declare function handleDepartmentSpending(task: TaskInput): Promise<TaskOutput>;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleDepartmentSpending = handleDepartmentSpending;
4
+ async function handleDepartmentSpending(task) {
5
+ const start = Date.now();
6
+ const input = task.input_payload;
7
+ const period = input.period || "Current Period";
8
+ const expenses = input.expenses || [
9
+ { department: "Engineering", category: "Salaries", amount: 85000 },
10
+ { department: "Engineering", category: "Software", amount: 12000 },
11
+ { department: "Engineering", category: "Hardware", amount: 8500 },
12
+ { department: "Marketing", category: "Salaries", amount: 45000 },
13
+ { department: "Marketing", category: "Advertising", amount: 25000 },
14
+ { department: "Marketing", category: "Events", amount: 8000 },
15
+ { department: "Sales", category: "Salaries", amount: 62000 },
16
+ { department: "Sales", category: "Travel", amount: 15000 },
17
+ { department: "Sales", category: "Entertainment", amount: 5000 },
18
+ { department: "Operations", category: "Rent", amount: 8500 },
19
+ { department: "Operations", category: "Utilities", amount: 2500 },
20
+ { department: "Operations", category: "Supplies", amount: 1800 },
21
+ { department: "HR", category: "Salaries", amount: 35000 },
22
+ { department: "HR", category: "Benefits Admin", amount: 4500 },
23
+ { department: "HR", category: "Recruiting", amount: 7500 },
24
+ ];
25
+ let grandTotal = 0;
26
+ const byDept = {};
27
+ for (const exp of expenses) {
28
+ grandTotal += exp.amount;
29
+ if (!byDept[exp.department])
30
+ byDept[exp.department] = { total: 0, categories: {}, count: 0 };
31
+ byDept[exp.department].total += exp.amount;
32
+ byDept[exp.department].count += 1;
33
+ byDept[exp.department].categories[exp.category] = (byDept[exp.department].categories[exp.category] || 0) + exp.amount;
34
+ }
35
+ grandTotal = round(grandTotal);
36
+ const deptSummary = Object.entries(byDept)
37
+ .sort((a, b) => b[1].total - a[1].total)
38
+ .map(([name, data]) => ({
39
+ department: name,
40
+ total: round(data.total),
41
+ pct: round((data.total / grandTotal) * 100),
42
+ count: data.count,
43
+ categories: Object.entries(data.categories).sort((a, b) => b[1] - a[1]).map(([cat, amt]) => ({ category: cat, amount: round(amt) })),
44
+ }));
45
+ const csv = [
46
+ "DEPARTMENT SPENDING REPORT",
47
+ `Period: ${period}`,
48
+ `Generated: ${new Date().toISOString().split("T")[0]}`,
49
+ `Grand Total: ${fmt(grandTotal)}`,
50
+ "",
51
+ "SUMMARY BY DEPARTMENT",
52
+ "Department,Total Spending,% of Total,Line Items",
53
+ ...deptSummary.map(d => `${esc(d.department)},${fmt(d.total)},${d.pct}%,${d.count}`),
54
+ "",
55
+ ];
56
+ // Add breakdown for each department
57
+ for (const dept of deptSummary) {
58
+ csv.push(`${dept.department.toUpperCase()} BREAKDOWN`);
59
+ csv.push("Category,Amount,% of Department");
60
+ for (const cat of dept.categories) {
61
+ csv.push(`${esc(cat.category)},${fmt(cat.amount)},${round((cat.amount / dept.total) * 100)}%`);
62
+ }
63
+ csv.push(`Department Total,${fmt(dept.total)},100%`);
64
+ csv.push("");
65
+ }
66
+ csv.push("GRAND TOTAL");
67
+ csv.push(`Total All Departments,${fmt(grandTotal)}`);
68
+ csv.push(`Total Departments,${deptSummary.length}`);
69
+ csv.push(`Total Line Items,${expenses.length}`);
70
+ csv.push(`Largest Department,${deptSummary[0]?.department || "N/A"} (${fmt(deptSummary[0]?.total || 0)})`);
71
+ csv.push(`Smallest Department,${deptSummary[deptSummary.length - 1]?.department || "N/A"} (${fmt(deptSummary[deptSummary.length - 1]?.total || 0)})`);
72
+ return {
73
+ text: `Department Spending: ${deptSummary.length} departments | Total: ${fmt(grandTotal)} | Largest: ${deptSummary[0]?.department}`,
74
+ output_csv: csv.join("\n"),
75
+ summary: { grand_total: grandTotal, department_count: deptSummary.length },
76
+ by_department: deptSummary,
77
+ chart_data: { chart_type: "pie", title: `Spending by Department — ${period}`, labels: deptSummary.map(d => d.department), datasets: [{ label: "Spending", data: deptSummary.map(d => d.total) }] },
78
+ duration_ms: Date.now() - start,
79
+ };
80
+ }
81
+ function round(n) { return Math.round(n * 100) / 100; }
82
+ function fmt(n) { return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }
83
+ function esc(v) { return v.includes(",") ? `"${v}"` : v; }
@@ -0,0 +1,2 @@
1
+ import type { TaskInput, TaskOutput } from "./index.js";
2
+ export declare function handleDepreciation(task: TaskInput): Promise<TaskOutput>;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleDepreciation = handleDepreciation;
4
+ async function handleDepreciation(task) {
5
+ const start = Date.now();
6
+ const input = task.input_payload;
7
+ const asOf = input.as_of_date ? new Date(input.as_of_date) : new Date();
8
+ const assets = input.assets || [
9
+ { name: "Office Furniture", cost: 15000, salvage_value: 1500, useful_life_years: 7, date_acquired: "2023-06-15", method: "straight-line" },
10
+ { name: "MacBook Pro Fleet (10)", cost: 35000, salvage_value: 5000, useful_life_years: 3, date_acquired: "2025-01-10", method: "straight-line" },
11
+ { name: "Server Equipment", cost: 28000, salvage_value: 3000, useful_life_years: 5, date_acquired: "2024-03-01", method: "double-declining" },
12
+ { name: "Company Vehicle", cost: 42000, salvage_value: 8000, useful_life_years: 5, date_acquired: "2024-09-01", method: "straight-line" },
13
+ { name: "HVAC System", cost: 18000, salvage_value: 2000, useful_life_years: 10, date_acquired: "2022-01-01", method: "straight-line" },
14
+ ];
15
+ let totalCost = 0, totalAccumDep = 0, totalCurrentYearDep = 0;
16
+ const schedule = assets.map(asset => {
17
+ const method = asset.method || "straight-line";
18
+ const depreciable = asset.cost - asset.salvage_value;
19
+ const acquired = new Date(asset.date_acquired);
20
+ const monthsOwned = Math.max(0, (asOf.getFullYear() - acquired.getFullYear()) * 12 + (asOf.getMonth() - acquired.getMonth()));
21
+ const yearsOwned = monthsOwned / 12;
22
+ let annualDep;
23
+ let accumDep;
24
+ let currentYearDep;
25
+ if (method === "straight-line") {
26
+ annualDep = round(depreciable / asset.useful_life_years);
27
+ accumDep = round(Math.min(annualDep * yearsOwned, depreciable));
28
+ currentYearDep = yearsOwned >= asset.useful_life_years ? 0 : annualDep;
29
+ }
30
+ else {
31
+ // Double declining balance
32
+ const rate = 2 / asset.useful_life_years;
33
+ let bookValue = asset.cost;
34
+ accumDep = 0;
35
+ currentYearDep = 0;
36
+ for (let y = 0; y < Math.ceil(yearsOwned); y++) {
37
+ const dep = Math.min(round(bookValue * rate), bookValue - asset.salvage_value);
38
+ if (dep <= 0)
39
+ break;
40
+ if (y < Math.floor(yearsOwned)) {
41
+ accumDep += dep;
42
+ bookValue -= dep;
43
+ }
44
+ if (y === Math.floor(yearsOwned)) {
45
+ currentYearDep = dep;
46
+ }
47
+ }
48
+ annualDep = round(asset.cost * (2 / asset.useful_life_years));
49
+ accumDep = round(accumDep);
50
+ }
51
+ const bookValue = round(asset.cost - accumDep);
52
+ const fullyDepreciated = accumDep >= depreciable;
53
+ const remainingLife = Math.max(0, round(asset.useful_life_years - yearsOwned));
54
+ totalCost += asset.cost;
55
+ totalAccumDep += accumDep;
56
+ totalCurrentYearDep += currentYearDep;
57
+ return { name: asset.name, cost: asset.cost, salvage: asset.salvage_value, useful_life: asset.useful_life_years, method, date_acquired: asset.date_acquired, annual_depreciation: annualDep, accumulated_depreciation: accumDep, current_year_depreciation: round(currentYearDep), book_value: bookValue, remaining_life: remainingLife, fully_depreciated: fullyDepreciated };
58
+ });
59
+ totalCost = round(totalCost);
60
+ totalAccumDep = round(totalAccumDep);
61
+ totalCurrentYearDep = round(totalCurrentYearDep);
62
+ const totalBookValue = round(totalCost - totalAccumDep);
63
+ const csv = [
64
+ "DEPRECIATION SCHEDULE",
65
+ `As of: ${asOf.toISOString().split("T")[0]}`,
66
+ "",
67
+ "ASSET DETAIL",
68
+ "Asset,Cost,Salvage,Useful Life,Method,Date Acquired,Annual Dep,Accum Dep,Book Value,Remaining Life,Status",
69
+ ...schedule.map(a => `${esc(a.name)},${fmt(a.cost)},${fmt(a.salvage)},${a.useful_life} yr,${a.method},${a.date_acquired},${fmt(a.annual_depreciation)},${fmt(a.accumulated_depreciation)},${fmt(a.book_value)},${a.remaining_life.toFixed(1)} yr,${a.fully_depreciated ? "Fully Depreciated" : "Active"}`),
70
+ "",
71
+ "CURRENT YEAR DEPRECIATION EXPENSE",
72
+ "Asset,Current Year Depreciation",
73
+ ...schedule.filter(a => a.current_year_depreciation > 0).map(a => `${esc(a.name)},${fmt(a.current_year_depreciation)}`),
74
+ `Total Current Year,${fmt(totalCurrentYearDep)}`,
75
+ "",
76
+ "SUMMARY",
77
+ `Total Asset Cost,${fmt(totalCost)}`,
78
+ `Total Accumulated Depreciation,${fmt(totalAccumDep)}`,
79
+ `Total Book Value,${fmt(totalBookValue)}`,
80
+ `Current Year Depreciation Expense,${fmt(totalCurrentYearDep)}`,
81
+ `Active Assets,${schedule.filter(a => !a.fully_depreciated).length}`,
82
+ `Fully Depreciated Assets,${schedule.filter(a => a.fully_depreciated).length}`,
83
+ ];
84
+ return {
85
+ text: `Depreciation Schedule: ${schedule.length} assets | Book Value: ${fmt(totalBookValue)} | Current Year: ${fmt(totalCurrentYearDep)}`,
86
+ output_csv: csv.join("\n"),
87
+ summary: { total_cost: totalCost, total_accum_dep: totalAccumDep, total_book_value: totalBookValue, current_year: totalCurrentYearDep, asset_count: schedule.length },
88
+ duration_ms: Date.now() - start,
89
+ };
90
+ }
91
+ function round(n) { return Math.round(n * 100) / 100; }
92
+ function fmt(n) { return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }
93
+ function esc(v) { return v.includes(",") ? `"${v}"` : v; }