Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2af2b28d84 | |||
| 76c6eefb36 | |||
| 2b355bf057 | |||
| 6759916323 | |||
| 6124a6d582 | |||
| 1d50da987f |
@@ -1,7 +1,2 @@
|
|||||||
.git/
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
.github/
|
|
||||||
.gitignore
|
|
||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|||||||
@@ -43,12 +43,6 @@ jobs:
|
|||||||
# set latest tag for default branch
|
# set latest tag for default branch
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Build and push the Docker image
|
- name: Build and push the Docker image
|
||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -58,4 +52,3 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
|
|||||||
+19
-34
@@ -1,32 +1,25 @@
|
|||||||
# Use an official Node.js runtime as a parent image
|
# Use an official Node.js runtime as a parent image
|
||||||
FROM node:22
|
FROM node:22.16.0-alpine3.21
|
||||||
|
|
||||||
RUN apt-get update -qq -y && \
|
RUN apk add --no-cache \
|
||||||
apt-get install -y \
|
alsa-lib \
|
||||||
libasound2 \
|
at-spi2-atk \
|
||||||
libatk-bridge2.0-0 \
|
gtk+3.0 \
|
||||||
libgtk-4-1 \
|
nss \
|
||||||
libnss3 \
|
|
||||||
xdg-utils \
|
xdg-utils \
|
||||||
wget && \
|
wget \
|
||||||
wget -q -O chrome-linux64.zip https://storage.googleapis.com/chrome-for-testing-public/131.0.6778.204/linux64/chrome-linux64.zip && \
|
unzip \
|
||||||
unzip chrome-linux64.zip && \
|
chromium=136.0.7103.113-r0 \
|
||||||
rm chrome-linux64.zip && \
|
chromium-chromedriver=136.0.7103.113-r0
|
||||||
mv chrome-linux64 /opt/chrome/ && \
|
|
||||||
ln -s /opt/chrome/chrome /usr/local/bin/ && \
|
|
||||||
wget -q -O chromedriver-linux64.zip https://storage.googleapis.com/chrome-for-testing-public/131.0.6778.204/linux64/chromedriver-linux64.zip && \
|
|
||||||
unzip -j chromedriver-linux64.zip chromedriver-linux64/chromedriver && \
|
|
||||||
rm chromedriver-linux64.zip && \
|
|
||||||
mv chromedriver /usr/local/bin/
|
|
||||||
|
|
||||||
# Don't run as root
|
|
||||||
USER node
|
|
||||||
|
|
||||||
# Set the working directory in the container
|
# Set the working directory in the container
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
# Create the cache directory
|
# Create the cache directory and set ownership (as root)
|
||||||
RUN mkdir -p ./cache && chown node:node ./cache
|
RUN mkdir -p ./cache && chown -R node:node ./cache
|
||||||
|
|
||||||
|
# Don't run as root
|
||||||
|
USER node
|
||||||
|
|
||||||
# Define environment variables
|
# Define environment variables
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -49,9 +42,9 @@ ENV ACTUAL_CACHE_DIR="./cache"
|
|||||||
# optional, name of the payee for added interest transactions
|
# optional, name of the payee for added interest transactions
|
||||||
ENV INTEREST_PAYEE_NAME="Loan Interest"
|
ENV INTEREST_PAYEE_NAME="Loan Interest"
|
||||||
|
|
||||||
# optional, name of the payee for added investment transactions
|
# optional, name of the payee for added interest transactions
|
||||||
ENV INVESTMENT_PAYEE_NAME="Investment"
|
ENV INVESTMENT_PAYEE_NAME="Investment"
|
||||||
# optional, name of the category group for added investment tracking transactions
|
# optional, name of the cateogry group for added investment tracking transactions
|
||||||
ENV INVESTMENT_CATEGORY_GROUP_NAME="Income"
|
ENV INVESTMENT_CATEGORY_GROUP_NAME="Income"
|
||||||
# optional, name of the category for added investment tracking transactions
|
# optional, name of the category for added investment tracking transactions
|
||||||
ENV INVESTMENT_CATEGORY_NAME="Investment"
|
ENV INVESTMENT_CATEGORY_NAME="Investment"
|
||||||
@@ -59,27 +52,19 @@ ENV INVESTMENT_CATEGORY_NAME="Investment"
|
|||||||
# optional, for logging into SimpleFIN
|
# optional, for logging into SimpleFIN
|
||||||
ENV SIMPLEFIN_CREDENTIALS=""
|
ENV SIMPLEFIN_CREDENTIALS=""
|
||||||
|
|
||||||
# optional, name of the payee for Zestimate entries
|
|
||||||
ENV ZESTIMATE_PAYEE_NAME="Zestimate"
|
|
||||||
|
|
||||||
# optional, name of the payee for KBB entries
|
|
||||||
ENV KBB_PAYEE_NAME="KBB"
|
|
||||||
|
|
||||||
# optional, for retrieving Bitcoin Price (these default to Kraken USD)
|
# optional, for retrieving Bitcoin Price (these default to Kraken USD)
|
||||||
ENV BITCOIN_PRICE_URL="https://api.kraken.com/0/public/Ticker?pair=xbtusd"
|
ENV BITCOIN_PRICE_URL="https://api.kraken.com/0/public/Ticker?pair=xbtusd"
|
||||||
ENV BITCOIN_PRICE_JSON_PATH="result.XXBTZUSD.c[0]"
|
ENV BITCOIN_PRICE_JSON_PATH="result.XXBTZUSD.c[0]"
|
||||||
ENV BITCOIN_PAYEE_NAME="Bitcoin Price Change"
|
ENV BITCOIN_PAYEE_NAME="Bitcoin Price Change"
|
||||||
|
|
||||||
#optional, RentCast API key for fetching property data
|
|
||||||
ENV RENTCAST_API_KEY=""
|
|
||||||
ENV RENTCAST_PAYEE_NAME="RentCast"
|
|
||||||
|
|
||||||
VOLUME ./cache
|
VOLUME ./cache
|
||||||
|
|
||||||
# Copy the current directory contents into the container at /usr/src/app
|
# Copy the current directory contents into the container at /usr/src/app
|
||||||
|
# This should happen after WORKDIR is set and USER is node
|
||||||
COPY --chown=node:node . .
|
COPY --chown=node:node . .
|
||||||
|
|
||||||
# Install any needed packages specified in package.json
|
# Install any needed packages specified in package.json
|
||||||
|
# This should run as the node user
|
||||||
RUN npm install && npm update
|
RUN npm install && npm update
|
||||||
|
|
||||||
# Run the app when the container launches
|
# Run the app when the container launches
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ ACTUAL_CACHE_DIR="./cache"
|
|||||||
# optional, name of the payee for added interest transactions
|
# optional, name of the payee for added interest transactions
|
||||||
INTEREST_PAYEE_NAME="Loan Interest"
|
INTEREST_PAYEE_NAME="Loan Interest"
|
||||||
|
|
||||||
# optional, name of the payee for added investment transactions
|
# optional, name of the payee for added interest transactions
|
||||||
INVESTMENT_PAYEE_NAME="Investment"
|
INVESTMENT_PAYEE_NAME="Investment"
|
||||||
# optional, name of the category group for added investment tracking transactions
|
# optional, name of the category group for added investment tracking transactions
|
||||||
INVESTMENT_CATEGORY_GROUP_NAME="Income"
|
INVESTMENT_CATEGORY_GROUP_NAME="Income"
|
||||||
@@ -72,23 +72,6 @@ RENTCAST_API_KEY="<Rentcast API key>"
|
|||||||
RENTCAST_PAYEE_NAME="RentCast"
|
RENTCAST_PAYEE_NAME="RentCast"
|
||||||
```
|
```
|
||||||
|
|
||||||
### OIDC Auth Provider Support
|
|
||||||
|
|
||||||
When using OIDC, in your Actual Budget server config you must be able to log in
|
|
||||||
with a password.
|
|
||||||
|
|
||||||
Set the following in your **server config** (not in your helpers config!), then
|
|
||||||
on initial login you must set a password:
|
|
||||||
|
|
||||||
```
|
|
||||||
ACTUAL_OPENID_AUTH_METHOD=openid
|
|
||||||
ACTUAL_LOGIN_METHOD=openid
|
|
||||||
ACTUAL_ALLOWED_LOGIN_METHODS=openid,password,header
|
|
||||||
ACTUAL_OPENID_ENFORCE=false
|
|
||||||
```
|
|
||||||
|
|
||||||
Then configure the helpers for password login, as shown above.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Run `npm install` to install any required dependencies.
|
Run `npm install` to install any required dependencies.
|
||||||
@@ -185,9 +168,7 @@ interest transaction on the 28th of the month, set the account note to
|
|||||||
By default, interest is calculated using the 30/360 method where interest is
|
By default, interest is calculated using the 30/360 method where interest is
|
||||||
computed monthly using 30/360 (or 1/12) of the interest rate. If you need to
|
computed monthly using 30/360 (or 1/12) of the interest rate. If you need to
|
||||||
compute interest using the ACTUAL/ACTUAL method, set `interest:actual` in the
|
compute interest using the ACTUAL/ACTUAL method, set `interest:actual` in the
|
||||||
note. If you need to compute interest daily, set `interest:daily`. For most
|
note. If you need to compute interest daily, set `interest:daily`.
|
||||||
student loans, you probably want simple dailiy interest
|
|
||||||
`interest:daily-simple`.
|
|
||||||
|
|
||||||
You can optionally change the payee used for the interest transactions by
|
You can optionally change the payee used for the interest transactions by
|
||||||
setting `INTEREST_PAYEE_NAME` in the `.env` file.
|
setting `INTEREST_PAYEE_NAME` in the `.env` file.
|
||||||
@@ -409,43 +390,3 @@ node sync-bitcoin.js
|
|||||||
```
|
```
|
||||||
|
|
||||||
It is recommended to run this script once per day or week.
|
It is recommended to run this script once per day or week.
|
||||||
|
|
||||||
### Tracking Crypto Prices
|
|
||||||
|
|
||||||
This script tracks the value of any cryptocurrency. Similar to the Bitcoin
|
|
||||||
script, it adds new transactions to keep the account balance equal to the
|
|
||||||
latest value. Values are retrieved from the Kraken API, but the script is
|
|
||||||
customizable to use other APIs if needed.
|
|
||||||
|
|
||||||
To use, set these tags in the account notes:
|
|
||||||
|
|
||||||
- `CRYPTO:<symbol>,<symbol>,...` - where each symbol is a cryptocurrency
|
|
||||||
you want to track, e.g. `CRYPTO:BTC,ETH,DOGE`
|
|
||||||
- `<symbol>:<amount>[,<krakenPair>]` - where `<symbol>` is the cryptocurrency
|
|
||||||
symbol and `<amount>` is the amount you own. If using Kraken, specify the Kraken
|
|
||||||
pair to use for the price. Obtain this by visiting
|
|
||||||
[https://api.kraken.com/0/public/Ticker?pair=<symbol\>usd](https://api.kraken.com/0/public/Ticker?pair=btcusd)
|
|
||||||
and locating the key under `result` that matches your symbol.
|
|
||||||
|
|
||||||
For example, if you own 0.01 BTC and 1 ETH, your account note would look like:
|
|
||||||
```
|
|
||||||
CRYPTO:BTC,ETH
|
|
||||||
BTC:0.01,XXBTZUSD
|
|
||||||
ETH:1,XXETHZUSD
|
|
||||||
```
|
|
||||||
|
|
||||||
To use a different API, set the following in your `.env` file: (example using Bitcoin)
|
|
||||||
```shell
|
|
||||||
CRYPTO_PRICE_URL_BTC="https://api.kraken.com/0/public/Ticker?pair=xbtgbp"
|
|
||||||
CRYPTO_PRICE_JSON_PATH_BTC="result.XXBTZGBP.c[0]"
|
|
||||||
```
|
|
||||||
|
|
||||||
To run:
|
|
||||||
|
|
||||||
```console
|
|
||||||
node sync-crypto.js
|
|
||||||
```
|
|
||||||
|
|
||||||
It is recommended to run this script once per day or week. If you are retrieving
|
|
||||||
BTC prices using this script, DO NOT run the `sync-bitcoin.js` script as well, as it will
|
|
||||||
set the account balance to the value of your BTC holdings only.
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
v26.5.2
|
v25.5.0
|
||||||
|
|||||||
+2
-10
@@ -41,15 +41,12 @@ function daysInYear(year) {
|
|||||||
|
|
||||||
const lastDate = await getLastTransactionDate(account, cutoff);
|
const lastDate = await getLastTransactionDate(account, cutoff);
|
||||||
if (!lastDate) continue;
|
if (!lastDate) continue;
|
||||||
const daysPassed = Math.round(
|
const daysPassed = Math.floor((interestTransactionDate - new Date(lastDate)) / 86400000);
|
||||||
(interestTransactionDate.setHours(0, 0, 0, 0) - new Date(lastDate).setHours(0, 0, 0, 0)) / 86400000
|
|
||||||
);
|
|
||||||
|
|
||||||
let period = 12;
|
let period = 12;
|
||||||
let numPeriods = 1
|
let numPeriods = 1
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'daily':
|
case 'daily':
|
||||||
case 'daily-simple':
|
|
||||||
period = daysInYear(interestTransactionDate.getFullYear());
|
period = daysInYear(interestTransactionDate.getFullYear());
|
||||||
numPeriods = daysPassed;
|
numPeriods = daysPassed;
|
||||||
break;
|
break;
|
||||||
@@ -61,12 +58,7 @@ function daysInYear(year) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const balance = await getAccountBalance(account, interestTransactionDate);
|
const balance = await getAccountBalance(account, interestTransactionDate);
|
||||||
|
const compoundedInterest = Math.round(balance * (Math.pow(1 + interestRate / period, numPeriods) - 1));
|
||||||
let compoundedInterest;
|
|
||||||
if (kind == 'daily-simple')
|
|
||||||
compoundedInterest = Math.round(balance * (interestRate / 365) * numPeriods);
|
|
||||||
else
|
|
||||||
compoundedInterest = Math.round(balance * (Math.pow(1 + interestRate / period, numPeriods) - 1));
|
|
||||||
|
|
||||||
interestRate = showPercent(interestRate);
|
interestRate = showPercent(interestRate);
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
const api = require('@actual-app/api');
|
|
||||||
const { closeBudget, ensurePayee, getAccountBalance, getAccountNote, getTagValue, openBudget, showPercent, sleep,
|
|
||||||
getBudgetMonth, getCategoryGroups, getCategories } = require('./utils');
|
|
||||||
require("dotenv").config();
|
|
||||||
|
|
||||||
(async function () {
|
|
||||||
await openBudget();
|
|
||||||
|
|
||||||
var today = new Date();
|
|
||||||
var yyyy = today.getFullYear();
|
|
||||||
var mm = (today.getMonth() + 1).toString().padStart(2, '0');
|
|
||||||
var budgetMonth = yyyy + '-' + mm;
|
|
||||||
const budget = await api.getBudgetMonth(budgetMonth);
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
console.log('~~!!'); // Make it easy to parse for e-mail
|
|
||||||
|
|
||||||
console.log('Budget for Month: ' + budgetMonth);
|
|
||||||
console.log('Total Budget: $' + (budget.totalBudgeted * .01).toFixed(2).padStart(8, ' '));
|
|
||||||
console.log('Income: $' + (budget.totalIncome * .01).toFixed(2).padStart(8, ' '));
|
|
||||||
console.log('Spent: $' + (budget.totalSpent * .01).toFixed(2).padStart(8, ' '));
|
|
||||||
console.log('Balance: $' + (budget.totalBalance * .01).toFixed(2).padStart(8, ' '));
|
|
||||||
console.log('----------------------------------------------------');
|
|
||||||
|
|
||||||
const categoryGroups = await api.getCategoryGroups();
|
|
||||||
for (const categoryGroup of budget.categoryGroups) {
|
|
||||||
if (categoryGroup.hidden || categoryGroup.budgeted == 0 || categoryGroup.is_income) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const categorySpentPercent = ((categoryGroup.spent * .01) / (categoryGroup.budgeted * .01) * -100);
|
|
||||||
const categoryBalancePercent = ((categoryGroup.balance * .01) / (categoryGroup.budgeted * .01) * 10);
|
|
||||||
|
|
||||||
console.log(categoryGroup.name.padEnd(21, ' ') + ' ' + categorySpentPercent.toFixed(1).trim().padStart(7, ' ') + '% spent of $' + (categoryGroup.budgeted * .01).toFixed(2).trim().padStart(8, ' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('!!~~'); // Make it easy to parse for e-mail
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
var transactionDate = new Date();
|
|
||||||
transactionDate.setDate(transactionDate.getDate() - 1);
|
|
||||||
let txDate = transactionDate.toISOString().substring(0, 10);
|
|
||||||
console.log('Transactions on ' + txDate);
|
|
||||||
console.log('DATE ACCOUNT PAYEE NOTES / MEMO AMOUNT');
|
|
||||||
console.log('-------------------------------------------------------------------------------------------------------------------------');
|
|
||||||
const payees = await api.getPayees();
|
|
||||||
const accounts = await api.getAccounts();
|
|
||||||
( await api.runQuery(
|
|
||||||
api.q('transactions')
|
|
||||||
.filter({
|
|
||||||
date: { $gte: txDate },
|
|
||||||
})
|
|
||||||
.select('*')
|
|
||||||
)
|
|
||||||
).data.map(row => {
|
|
||||||
let payeeName = payees.find(p => p.id === row.payee)?.name;
|
|
||||||
let accountName = accounts.find(a => a.id === row.account)?.name;
|
|
||||||
console.log(row.date + ' ' +
|
|
||||||
fixedStringLength(accountName, 20) + ' ' +
|
|
||||||
fixedStringLength(payeeName, 25) + ' ' +
|
|
||||||
fixedStringLength(row.notes, 45) + ' ' +
|
|
||||||
('$' + (row.amount / 100).toFixed(2).trim()).padStart(11, ' '));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
await closeBudget();
|
|
||||||
})();
|
|
||||||
|
|
||||||
function fixedStringLength(str, length) {
|
|
||||||
if (str === null) {
|
|
||||||
str = '';
|
|
||||||
}
|
|
||||||
return str.padEnd(length).substring(0, length);
|
|
||||||
}
|
|
||||||
+1
-1
@@ -13,7 +13,7 @@ ACTUAL_CACHE_DIR="./cache"
|
|||||||
# optional, name of the payee for added interest transactions
|
# optional, name of the payee for added interest transactions
|
||||||
INTEREST_PAYEE_NAME="Loan Interest"
|
INTEREST_PAYEE_NAME="Loan Interest"
|
||||||
|
|
||||||
# optional, name of the payee for added investment transactions
|
# optional, name of the payee for added interest transactions
|
||||||
INVESTMENT_PAYEE_NAME="Investment"
|
INVESTMENT_PAYEE_NAME="Investment"
|
||||||
# optional, name of the category group for added investment tracking transactions
|
# optional, name of the category group for added investment tracking transactions
|
||||||
INVESTMENT_CATEGORY_GROUP_NAME="Income"
|
INVESTMENT_CATEGORY_GROUP_NAME="Income"
|
||||||
|
|||||||
+2
-3
@@ -7,13 +7,12 @@ timedatectl set-timezone America/Chicago
|
|||||||
# Perform update / upgrade of all existing packages and repos
|
# Perform update / upgrade of all existing packages and repos
|
||||||
apt update && apt upgrade -y
|
apt update && apt upgrade -y
|
||||||
|
|
||||||
# Install curl and git if not present
|
# Install all identified dependencies
|
||||||
apt install curl git ca-certificates fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 \
|
apt install curl git ca-certificates fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 \
|
||||||
libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 \
|
libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 \
|
||||||
libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \
|
libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \
|
||||||
libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 \
|
libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 \
|
||||||
libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils \
|
libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils -y
|
||||||
apache2 php php-curl php-json php-cgi libapache2-mod-php -y
|
|
||||||
|
|
||||||
# Add NodeSource repository (replace 22.x with your desired version, e.g., 20.x, 18.x)
|
# Add NodeSource repository (replace 22.x with your desired version, e.g., 20.x, 18.x)
|
||||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||||
|
|||||||
Generated
+247
-1184
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actual-app/api": "latest",
|
"@actual-app/api": "latest",
|
||||||
|
"better-sqlite3": "^11.7.2",
|
||||||
"chromedriver": "^131.0.4",
|
"chromedriver": "^131.0.4",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"jsdom": "^24.1.0",
|
"jsdom": "^24.1.0",
|
||||||
|
|||||||
-111
@@ -1,111 +0,0 @@
|
|||||||
const { closeBudget, openBudget, getTransactions, getAccountNote, getAccountBalance, getTagValue, ensurePayee } = require('./utils');
|
|
||||||
const api = require('@actual-app/api');
|
|
||||||
|
|
||||||
function getValueAtPath(obj, path) {
|
|
||||||
const keys = path.split('.').filter(Boolean);
|
|
||||||
|
|
||||||
return keys.reduce((acc, key) => {
|
|
||||||
const match = key.match(/^([^\[\]]+)(\[(\d+)\])?$/);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const property = match[1];
|
|
||||||
const index = match[3];
|
|
||||||
|
|
||||||
acc = acc[property];
|
|
||||||
|
|
||||||
if (index !== undefined) {
|
|
||||||
acc = acc[parseInt(index, 10)];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
acc = acc[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCryptoPrice(crypto, krakenPath) {
|
|
||||||
const url = process.env[`CRYPTO_PRICE_URL_${crypto.toUpperCase()}`] || `https://api.kraken.com/0/public/Ticker?pair=${crypto}usd`;
|
|
||||||
if (!krakenPath && !process.env[`CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()}`]) {
|
|
||||||
console.error(`No Kraken path provided for ${crypto}. Please set CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()} environment variable or provide krakenPath argument.`);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const path = process.env[`CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()}`] || `result.${krakenPath}.c[0]`;
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
const json = await response.json();
|
|
||||||
return getValueAtPath(json, path);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error fetching price for ${crypto}:`, error);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
await openBudget();
|
|
||||||
const payeeId = await ensurePayee(process.env.CRYPTO_PAYEE_NAME || 'Crypto Price Change');
|
|
||||||
const accounts = await api.getAccounts();
|
|
||||||
for (const account of accounts) {
|
|
||||||
if (account.closed) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Need to set cutoff date to tomorrow to ensure we get the latest transactions
|
|
||||||
const cutoffDate = new Date();
|
|
||||||
cutoffDate.setDate(cutoffDate.getDate() + 1);
|
|
||||||
|
|
||||||
const note = await getAccountNote(account, cutoffDate);
|
|
||||||
|
|
||||||
// Get cryptos list from the note
|
|
||||||
let cryptos = getTagValue(note, 'CRYPTO', null);
|
|
||||||
if (!cryptos) continue;
|
|
||||||
cryptos = cryptos.split(',').map(c => c.trim()).filter(c => c.length > 0);
|
|
||||||
if (cryptos.length === 0) continue;
|
|
||||||
|
|
||||||
console.log(`Processing account: ${account.name}, Cryptos: ${cryptos.join(', ')}`);
|
|
||||||
|
|
||||||
let totalTargetBalance = 0;
|
|
||||||
for (const crypto of cryptos) {
|
|
||||||
const cryptoTag = getTagValue(note, crypto, 0.0);
|
|
||||||
if (!cryptoTag) continue;
|
|
||||||
cryptoTagSplit = cryptoTag.split(',');
|
|
||||||
|
|
||||||
// Get the amount from the first part of the tag
|
|
||||||
amount = parseFloat(cryptoTagSplit[0]);
|
|
||||||
if (isNaN(amount) || amount <= 0) continue;
|
|
||||||
console.log(`Crypto: ${crypto}, Amount: ${amount}`);
|
|
||||||
|
|
||||||
// Get Kraken path from the second part of the tag, if it exists
|
|
||||||
let krakenPath = cryptoTagSplit[1]
|
|
||||||
console.log(`Kraken Path: ${krakenPath}`);
|
|
||||||
|
|
||||||
// Get the crypto price
|
|
||||||
const cryptoPrice = await getCryptoPrice(crypto, krakenPath);
|
|
||||||
if (!cryptoPrice) {
|
|
||||||
console.error(`Unable to retrieve price for ${crypto}. Check your CRYPTO_PRICE_URL_${crypto.toUpperCase()} and CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()} environment variables`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Crypto: ${crypto}, Price: ${cryptoPrice}`);
|
|
||||||
|
|
||||||
const targetBalance = Math.round(cryptoPrice * amount * 100);
|
|
||||||
totalTargetBalance += targetBalance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we need to check if the total target balance is different from the current balance and update it
|
|
||||||
const currentBalance = await getAccountBalance(account, cutoffDate);
|
|
||||||
const diff = totalTargetBalance - currentBalance;
|
|
||||||
console.log(`Account: ${account.name}, Total Target Balance: ${totalTargetBalance}, Current Balance: ${currentBalance}, Diff: ${diff}`);
|
|
||||||
if (diff != 0) {
|
|
||||||
await api.importTransactions(account.id, [{
|
|
||||||
date: new Date(),
|
|
||||||
payee: payeeId,
|
|
||||||
amount: diff,
|
|
||||||
cleared: true,
|
|
||||||
reconciled: true,
|
|
||||||
notes: `Updated Crypto Price on ${new Date().toLocaleString()} (${cryptos.join(', ')})`,
|
|
||||||
}])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await closeBudget();
|
|
||||||
})();
|
|
||||||
+9
-17
@@ -16,21 +16,16 @@ const getCredentials = async () => {
|
|||||||
const response = await fetch(url, { method: 'post' });
|
const response = await fetch(url, { method: 'post' });
|
||||||
const api_url = await response.text();
|
const api_url = await response.text();
|
||||||
|
|
||||||
try {
|
const rest = api_url.split('//', 2)[1];
|
||||||
const rest = api_url.split('//', 2)[1];
|
const auth = rest.split('@', 1)[0];
|
||||||
const auth = rest.split('@', 1)[0];
|
const username = auth.split(':')[0];
|
||||||
const username = auth.split(':')[0];
|
const pw = auth.split(':')[1];
|
||||||
const pw = auth.split(':')[1];
|
|
||||||
|
|
||||||
const data = `${username}:${pw}`;
|
const data = `${username}:${pw}`;
|
||||||
const cache = process.env.ACTUAL_CACHE_DIR || './cache';
|
const cache = process.env.ACTUAL_CACHE_DIR || './cache';
|
||||||
fs.writeFileSync(cache + '/simplefin.credentials', data);
|
fs.writeFileSync(cache + '/simplefin.credentials', data);
|
||||||
console.log('SimpleFIN credentials:', data);
|
console.log('SimpleFIN credentials:', data);
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
|
||||||
console.log('Invalid SimpleFIN setup response:', api_url);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadCredentials = () => {
|
const loadCredentials = () => {
|
||||||
@@ -46,9 +41,6 @@ const getSimplefinBalances = async () => {
|
|||||||
let credentials = loadCredentials();
|
let credentials = loadCredentials();
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
credentials = await getCredentials();
|
credentials = await getCredentials();
|
||||||
if (!credentials) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const username = credentials.split(':')[0];
|
const username = credentials.split(':')[0];
|
||||||
const pw = credentials.split(':')[1];
|
const pw = credentials.split(':')[1];
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
const api = require('@actual-app/api');
|
|
||||||
const fs = require('fs');
|
|
||||||
const readline = require('readline');
|
|
||||||
const { closeBudget, ensurePayee, getAccountBalance, getAccountNote, getTagValue, openBudget, showPercent, sleep,
|
|
||||||
getBudgetMonth, getCategoryGroups, getCategories } = require('./utils');
|
|
||||||
require("dotenv").config();
|
|
||||||
|
|
||||||
(async function () {
|
|
||||||
await openBudget();
|
|
||||||
|
|
||||||
const fileStream = fs.createReadStream("/var/www/html/data/investment-data.txt");
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: fileStream,
|
|
||||||
crlfDelay: Infinity
|
|
||||||
});
|
|
||||||
|
|
||||||
const payeeId = await ensurePayee('Change in Market Value');
|
|
||||||
for await (const line of rl) {
|
|
||||||
const parts = line.split('=');
|
|
||||||
const accountBalance = await api.getAccountBalance(parts[0]);
|
|
||||||
const adjustment = (accountBalance - parts[1].replace(/\./g, '')) * -1;
|
|
||||||
if (adjustment == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(parts[0] + ' --> ' + parts[1] + ' --> $' + adjustment);
|
|
||||||
|
|
||||||
await api.importTransactions(parts[0], [{
|
|
||||||
date: new Date(),
|
|
||||||
payee: payeeId,
|
|
||||||
amount: adjustment,
|
|
||||||
cleared: true,
|
|
||||||
reconciled: true,
|
|
||||||
notes: `Change in Market Value`,
|
|
||||||
}]);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteFileIfExists('/var/www/html/data/investment-data.txt');
|
|
||||||
await closeBudget();
|
|
||||||
})();
|
|
||||||
|
|
||||||
function fixedStringLength(str, length) {
|
|
||||||
if (str === null) {
|
|
||||||
str = '';
|
|
||||||
}
|
|
||||||
return str.padEnd(length).substring(0, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteFileIfExists(filePath) {
|
|
||||||
try {
|
|
||||||
await fs.promises.unlink(filePath);
|
|
||||||
console.log(`File ${filePath} deleted successfully.`);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code === 'ENOENT') {
|
|
||||||
console.log(`File ${filePath} does not exist, so it was not deleted.`);
|
|
||||||
} else {
|
|
||||||
console.error('Error deleting file:', err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user