add apply-interest script
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
|
||||
This is a collection of useful scripts to help you manage your Actual Budget.
|
||||
|
||||
- [Requirements](#requirements)
|
||||
- [Configuration](#configuration)
|
||||
- [Installation](#installation)
|
||||
- Scripts:
|
||||
- [Loan Interest Calculator](#loan-interest-calculator)
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Actual Budget](https://actualbudget.org/)
|
||||
@@ -24,8 +30,40 @@ ACTUAL_FILE_PASSWORD="<file password>"
|
||||
|
||||
# optional, if you want to use a different cache directory
|
||||
ACTUAL_CACHE_DIR="./cache"
|
||||
|
||||
# optional, name of the payee for added interest transactions
|
||||
IMPORTER_INTEREST_PAYEE_NAME="Loan Interest"
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Run `npm install` to install any required dependencies.
|
||||
|
||||
## Scripts
|
||||
|
||||
### Loan Interest Calculator
|
||||
|
||||
This script calculates the interest for a loan account and adds the interest
|
||||
transactions to Actual Budget.
|
||||
|
||||
For each account that you want to automaitcally calculate interest for, you
|
||||
need to edit the account notes and add the following tags:
|
||||
|
||||
- `interestRate:0.0X` sets the interest rate to X percent (note: be sure to
|
||||
enter the rate as a decimal and not a percentage)
|
||||
- `interestDay:XX` sets the day of the month that the interest is calculated
|
||||
|
||||
As an example, if your loan is at 4.5% interest and you want to insert an
|
||||
interest transaction on the 28th of the month, set the account note to
|
||||
`interestRate:0.045 interestDay:28`.
|
||||
|
||||
You can optionally change the payee used for the interest transactions by
|
||||
setting `IMPORTER_INTEREST_PAYEE_NAME` in the `.env` file.
|
||||
|
||||
To run:
|
||||
|
||||
```console
|
||||
$ node apply-interest.js
|
||||
```
|
||||
|
||||
It is recommended to run this script once per month.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
const api = require('@actual-app/api');
|
||||
const { closeBudget, ensurePayee, getAccountBalance, getAccountNote, getLastTransactionDate, openBudget } = require('./utils');
|
||||
require("dotenv").config();
|
||||
|
||||
(async () => {
|
||||
await openBudget();
|
||||
|
||||
const payeeId = await ensurePayee(process.env.IMPORTER_INTEREST_PAYEE_NAME || 'Loan Interest');
|
||||
|
||||
const accounts = await api.getAccounts();
|
||||
for (const account of accounts) {
|
||||
if (account.closed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const note = await getAccountNote(account);
|
||||
|
||||
if (note) {
|
||||
if (note.indexOf('interestRate:') > -1 && note.indexOf('interestDay:') > -1) {
|
||||
const interestRate = parseFloat(note.split('interestRate:')[1].split(' ')[0]);
|
||||
const interestDay = parseInt(note.split('interestDay:')[1].split(' ')[0]);
|
||||
|
||||
const interestTransactionDate = new Date();
|
||||
if (interestTransactionDate.getDate() < interestDay) {
|
||||
interestTransactionDate.setMonth(interestTransactionDate.getMonth() - 1);
|
||||
}
|
||||
interestTransactionDate.setDate(interestDay);
|
||||
interestTransactionDate.setHours(5, 0, 0, 0);
|
||||
|
||||
const cutoff = new Date(interestTransactionDate);
|
||||
cutoff.setMonth(cutoff.getMonth() - 1);
|
||||
cutoff.setDate(cutoff.getDate() + 1);
|
||||
|
||||
const lastDate = await getLastTransactionDate(account, cutoff);
|
||||
if (!lastDate) continue;
|
||||
const daysPassed = Math.floor((interestTransactionDate - new Date(lastDate)) / 86400000);
|
||||
|
||||
const balance = await getAccountBalance(account, interestTransactionDate);
|
||||
const compoundedInterest = Math.round(balance * (Math.pow(1 + interestRate / 12, 1) - 1));
|
||||
|
||||
console.log(`== ${account.name} ==`);
|
||||
console.log(` -> Balance: ${balance}`);
|
||||
console.log(` as of ${lastDate}`);
|
||||
console.log(` -> # days: ${daysPassed}`);
|
||||
console.log(` -> Interest: ${compoundedInterest}`)
|
||||
|
||||
if (compoundedInterest) {
|
||||
await api.importTransactions(account.id, [{
|
||||
date: interestTransactionDate,
|
||||
payee: payeeId,
|
||||
amount: compoundedInterest,
|
||||
notes: `Interest for 1 month at ${100*interestRate}%`,
|
||||
}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await closeBudget();
|
||||
})();
|
||||
@@ -0,0 +1,127 @@
|
||||
const api = require('@actual-app/api');
|
||||
require("dotenv").config();
|
||||
|
||||
module.exports = {
|
||||
openBudget: async function () {
|
||||
const url = process.env.ACTUAL_SERVER_URL || '';
|
||||
const password = process.env.ACTUAL_SERVER_PASSWORD || '';
|
||||
const file_password = process.env.ACTUAL_FILE_PASSWORD || '';
|
||||
const sync_id = process.env.ACTUAL_SYNC_ID || '';
|
||||
const cache = process.env.IMPORTER_CACHE_DIR || './cache';
|
||||
|
||||
if (!url || !password || !sync_id) {
|
||||
console.error('Required settings for Actual not provided.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("connect");
|
||||
await api.init({ serverURL: url, password: password, dataDir: cache });
|
||||
|
||||
console.log("open file");
|
||||
if (file_password) {
|
||||
await api.downloadBudget(sync_id, { password: file_password, });
|
||||
} else {
|
||||
await api.downloadBudget(sync_id);
|
||||
}
|
||||
},
|
||||
|
||||
closeBudget: async function () {
|
||||
console.log("done");
|
||||
await api.shutdown();
|
||||
},
|
||||
|
||||
getAccountBalance: async function (account, cutoffDate=new Date()) {
|
||||
const data = await api.runQuery(
|
||||
api.q('transactions')
|
||||
.filter({
|
||||
'account': account.id,
|
||||
'date': { $lt: cutoffDate },
|
||||
})
|
||||
.calculate({ $sum: '$amount' })
|
||||
.options({ splits: 'grouped' })
|
||||
);
|
||||
return data.data;
|
||||
},
|
||||
|
||||
getLastTransactionDate: async function (account, cutoffDate=new Date()) {
|
||||
const data = await api.runQuery(
|
||||
api.q('transactions')
|
||||
.filter({
|
||||
'account': account.id,
|
||||
'date': { $lt: cutoffDate },
|
||||
'amount': { $gt: 0 },
|
||||
})
|
||||
.select('date')
|
||||
.orderBy({ 'date': 'desc' })
|
||||
.limit(1)
|
||||
.options({ splits: 'grouped' })
|
||||
);
|
||||
if (!data.data.length) {
|
||||
return undefined;
|
||||
}
|
||||
return data.data[0].date;
|
||||
},
|
||||
|
||||
ensurePayee: async function (payeeName) {
|
||||
const payees = await api.getPayees();
|
||||
let payeeId = payees.find(p => p.name === payeeName)?.id;
|
||||
if (!payeeId) {
|
||||
payeeId = await api.createPayee({ name: payeeName });
|
||||
}
|
||||
if (!payeeId) {
|
||||
console.error('Failed to create payee:', payeeName);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
|
||||
ensureCategory: async function (categoryName) {
|
||||
const categories = await api.getCategories();
|
||||
let categoryId = categories.find(c => c.name === categoryName)?.id;
|
||||
if (!categoryId) {
|
||||
categoryId = await api.createCategory({ name: categoryName });
|
||||
}
|
||||
if (!categoryId) {
|
||||
console.error('Failed to create category:', categoryName);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
|
||||
getTagValue: function (note, tag, defaultValue=undefined) {
|
||||
tag += ':'
|
||||
const tagIndex = note.indexOf(tag);
|
||||
if (tagIndex === -1) {
|
||||
return defaultValue;
|
||||
}
|
||||
return note.split(tag)[1].split(/[\s]/)[0]
|
||||
},
|
||||
|
||||
getNote: async function (id) {
|
||||
const notes = await api.runQuery(
|
||||
api.q('notes')
|
||||
.filter({ id })
|
||||
.select('*')
|
||||
);
|
||||
if (notes.data.length && notes.data[0].note) {
|
||||
return notes.data[0].note;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
getAccountNote: async function (account) {
|
||||
const notes = await api.runQuery(
|
||||
api.q('notes')
|
||||
.filter({ id: `account-${account.id}` })
|
||||
.select('*')
|
||||
);
|
||||
if (notes.data.length && notes.data[0].note) {
|
||||
return notes.data[0].note;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
sleep: function (ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user