n8n-nodes-actual-budget-category 0.1.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,38 @@
|
|
|
1
|
+
const { createExpense, initActual, shutdownActual } = require('../actual/CreateExpense.cjs');
|
|
2
|
+
|
|
3
|
+
class ActualBudget {
|
|
4
|
+
async execute() {
|
|
5
|
+
const items = this.getInputData();
|
|
6
|
+
const creds = await this.getCredentials('actualBudgetApi');
|
|
7
|
+
const results = [];
|
|
8
|
+
|
|
9
|
+
// ✅ INIT ONCE
|
|
10
|
+
await initActual({
|
|
11
|
+
serverURL: creds.serverUrl,
|
|
12
|
+
password: creds.password,
|
|
13
|
+
syncId: creds.syncId,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
for (let i = 0; i < items.length; i++) {
|
|
18
|
+
const result = await createExpense({
|
|
19
|
+
amount: this.getNodeParameter('amount', i),
|
|
20
|
+
categoryName: this.getNodeParameter('categoryName', i),
|
|
21
|
+
accountName: this.getNodeParameter('accountName', i),
|
|
22
|
+
payee: this.getNodeParameter('payee', i),
|
|
23
|
+
notes: this.getNodeParameter('notes', i),
|
|
24
|
+
importedId: `n8n-${Date.now()}-${i}`,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
results.push({ json: { success: true, ...result } });
|
|
28
|
+
}
|
|
29
|
+
} finally {
|
|
30
|
+
// ✅ SHUTDOWN ONCE
|
|
31
|
+
await shutdownActual();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return [results];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { ActualBudget };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class ActualBudgetApi {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.name = 'actualBudgetApi';
|
|
4
|
+
this.displayName = 'Actual Budget API';
|
|
5
|
+
|
|
6
|
+
this.properties = [
|
|
7
|
+
{
|
|
8
|
+
displayName: 'Server URL',
|
|
9
|
+
name: 'serverUrl',
|
|
10
|
+
type: 'string',
|
|
11
|
+
required: true,
|
|
12
|
+
placeholder: 'https://actual.example.com',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
displayName: 'Password',
|
|
16
|
+
name: 'password',
|
|
17
|
+
type: 'string',
|
|
18
|
+
typeOptions: { password: true },
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
displayName: 'Sync ID',
|
|
23
|
+
name: 'syncId',
|
|
24
|
+
type: 'string',
|
|
25
|
+
required: true,
|
|
26
|
+
description: 'Found in Settings → Advanced → Sync ID',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { ActualBudgetApi };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
const { init, shutdown, downloadBudget, getAccounts, getCategories, addTransactions } = require('@actual-app/api');
|
|
6
|
+
|
|
7
|
+
function normalize(s) {
|
|
8
|
+
return s.toLowerCase().trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class ActualBudget {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.description = {
|
|
14
|
+
displayName: 'Actual Budget',
|
|
15
|
+
name: 'actualBudget',
|
|
16
|
+
group: ['finance'],
|
|
17
|
+
version: 1,
|
|
18
|
+
description: 'Create a transaction in Actual Budget',
|
|
19
|
+
defaults: { name: 'Actual Budget' },
|
|
20
|
+
inputs: ['main'],
|
|
21
|
+
outputs: ['main'],
|
|
22
|
+
credentials: [{ name: 'actualBudgetApi', required: true }],
|
|
23
|
+
properties: [
|
|
24
|
+
{ displayName: 'Amount', name: 'amount', type: 'number', required: true },
|
|
25
|
+
{ displayName: 'Category Name', name: 'categoryName', type: 'string', required: true },
|
|
26
|
+
{ displayName: 'Account Name', name: 'accountName', type: 'string', required: true },
|
|
27
|
+
{ displayName: 'Payee', name: 'payee', type: 'string' },
|
|
28
|
+
{ displayName: 'Notes', name: 'notes', type: 'string' },
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async execute() {
|
|
34
|
+
const items = this.getInputData();
|
|
35
|
+
const creds = await this.getCredentials('actualBudgetApi');
|
|
36
|
+
const results = [];
|
|
37
|
+
|
|
38
|
+
// 🔑 FIX: ensure syncId is a string
|
|
39
|
+
const syncId = typeof creds.syncId === 'string' ? creds.syncId : creds.syncId?.value;
|
|
40
|
+
|
|
41
|
+
if (!syncId) {
|
|
42
|
+
throw new Error(`Invalid syncId: ${JSON.stringify(creds.syncId)}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// same behavior you already had
|
|
46
|
+
const dataDir = path.join(os.tmpdir(), 'actual-budget', syncId);
|
|
47
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
48
|
+
|
|
49
|
+
// INIT ONCE
|
|
50
|
+
await init({
|
|
51
|
+
serverURL: creds.serverUrl,
|
|
52
|
+
password: creds.password,
|
|
53
|
+
dataDir,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// open budget once
|
|
58
|
+
await downloadBudget({ syncId });
|
|
59
|
+
|
|
60
|
+
const accounts = await getAccounts();
|
|
61
|
+
const categories = await getCategories();
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < items.length; i++) {
|
|
64
|
+
const amount = this.getNodeParameter('amount', i);
|
|
65
|
+
const categoryName = this.getNodeParameter('categoryName', i);
|
|
66
|
+
const accountName = this.getNodeParameter('accountName', i);
|
|
67
|
+
const payee = this.getNodeParameter('payee', i);
|
|
68
|
+
const notes = this.getNodeParameter('notes', i);
|
|
69
|
+
|
|
70
|
+
const account = accounts.find(a => normalize(a.name) === normalize(accountName));
|
|
71
|
+
if (!account) {
|
|
72
|
+
throw new Error(`Account not found: ${accountName}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const category = categories.find(c => normalize(c.name) === normalize(categoryName));
|
|
76
|
+
if (!category) {
|
|
77
|
+
throw new Error(`Category not found: ${categoryName}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await addTransactions(account.id, [
|
|
81
|
+
{
|
|
82
|
+
amount: -Math.round(amount * 100),
|
|
83
|
+
category: category.id,
|
|
84
|
+
payee_name: payee,
|
|
85
|
+
notes: notes || '',
|
|
86
|
+
date: new Date().toISOString().slice(0, 10),
|
|
87
|
+
imported_id: `n8n-${Date.now()}-${i}`,
|
|
88
|
+
},
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
results.push({
|
|
92
|
+
json: {
|
|
93
|
+
success: true,
|
|
94
|
+
accountId: account.id,
|
|
95
|
+
categoryId: category.id,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
// SHUTDOWN ONCE
|
|
101
|
+
await shutdown();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return [results];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { ActualBudget };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-actual-budget-category",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "n8n community node for Actual Budget",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"n8n-community-node",
|
|
8
|
+
"actual-budget",
|
|
9
|
+
"finance"
|
|
10
|
+
],
|
|
11
|
+
"author": "Sebastian Larrieu",
|
|
12
|
+
"main": "index.js",
|
|
13
|
+
"files": [
|
|
14
|
+
"nodes",
|
|
15
|
+
"credentials",
|
|
16
|
+
"actual"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node test/actual-budget-test.cjs"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@actual-app/api": "^25.12.0"
|
|
23
|
+
},
|
|
24
|
+
"n8n": {
|
|
25
|
+
"nodes": [
|
|
26
|
+
"nodes/ActualBudget.node.js"
|
|
27
|
+
],
|
|
28
|
+
"credentials": [
|
|
29
|
+
"credentials/ActualBudgetApi.credentials.js"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|