Skip to content
This repository has been archived by the owner on Jan 5, 2025. It is now read-only.

Commit

Permalink
First draft
Browse files Browse the repository at this point in the history
  • Loading branch information
rodriguestiago0 committed Jun 21, 2024
0 parents commit 57a8273
Show file tree
Hide file tree
Showing 19 changed files with 3,759 additions and 0 deletions.
34 changes: 34 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/

**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.next
**/.cache
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
**/build
**/dist
LICENSE
README.md
18 changes: 18 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Your Tink keys, which can be found in the Tink Dashboard.
# https://tink.com/

TINK_CLIENT_ID=
TINK_CLIENT_SECRET=
TINK_USER_ID=
TINK_ACTOR_ID=

# Comma-separated.
# both need to have the same length
TINK_ACCOUNT_MAP=
ACTUAL_ACCOUNT_MAP=

ACTUAL_SERVER_URL="server url"
ACTUAL_SERVER_PASSWORD="server password"
ACTUAL_SYNC_ID=

CRON_EXPRESSION=
44 changes: 44 additions & 0 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Docker Image Publish

on:
push:
branches: [ "main" ]
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ubuntu-latest

steps:
-
name: Checkout
uses: actions/checkout@v3
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v2

- name: Log into registry
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: rodriguestiago0/tinkactual


- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }} # Don't push on PR
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
.vscode/
.env
temp_data_actual/
31 changes: 31 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
ARG NODE_VERSION=20.11.1

FROM node:${NODE_VERSION}-alpine AS BUILD_IMAGE

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install --omit=dev
RUN npm ci --omit=dev

COPY . .

FROM node:${NODE_VERSION}-alpine AS RUNNER_IMAGE

WORKDIR /usr/src/app

COPY --from=BUILD_IMAGE /usr/src/app/node_modules ./node_modules
ADD . .
ADD package*.json ./


RUN chmod +x index-cron.js

ENV NODE_ENV production

# Run the application.
CMD node index-cron.js



21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 rodriguestiago0

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
45 changes: 45 additions & 0 deletions README.Docker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
### Building and running your application

When you're ready, start your application by running:
`docker compose up --build`.

Docker-comple.yaml example:
```
version: '3'
services:
actual_server:
container_name: tinkactual
image: docker.io/rodriguestiago0/tinkactual
ports:
- '5006:5006'
environment:
- PUID=1003
- PGID=100
- TZ=Europe/Lisbon
- TINK_CLIENT_ID=
- TINK_CLIENT_SECRET=
- TINK_USER_ID=
- TINK_ACTOR_ID=
- TINK_ACCOUNT_MAP= #comma separated vlue (Both TINK_ACCOUNT_MAP and ACTUAL_ACCOUNT_MAP need to have the same size)
- ACTUAL_ACCOUNT_MAP= #comma separated vlue
- ACTUAL_SERVER_URL=
- ACTUAL_SERVER_PASSWORD=
- ACTUAL_SYNC_ID=
- CRON_EXPRESSION= # default value is "0 */4 * * *"
restart: unless-stopped
```

```
docker run -d --name tinkactual \
- e 'TINK_CLIENT_ID=' \
- e 'TINK_CLIENT_SECRET=' \
- e 'TINK_USER_ID=' \
- e 'TINK_ACTOR_ID=' \
- e 'TINK_ACCOUNT_MAP=' \
- e 'ACTUAL_ACCOUNT_MAP=' \
- e 'ACTUAL_SERVER_URL= ' \
- e 'ACTUAL_SERVER_PASSWORD=' \
- e 'ACTUAL_SYNC_ID=' \
- e CRON_EXPRESSION= # default value is "0 */4 * '* *"' \
--restart=on-failure rodriguestiago0/tinkactual:latest
```
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# tinkactual

## Setup

- Clone this repo!
- Install dependencies: `npm ci`
- Copy `.sample.env` to `.env` and fill in the blanks
- Run `check`: `node index.js check`, this will check the balance between your Actual Budget account and the accounts setup on tink
- Run `import`: `node index.js import`, this will import all transactions to Actual

## Some things worth noting

The intial transaction import does not have a starting balance, so you will need to manually add that to Actual Budget.

You need to manually create the accounts inside Actual, and then map them to the accounts you setup in Tink.

## Commands


```
Usage
$ myedenredactual <command> <flags>
Commands & Options
ls List currently syncing accounts
import Sync bank accounts to Actual Budget
config Print the location of actualplaid the config file
--version Print the version of actualplaid being used
Options for all commands
--user, -u Specify the user to load configs for
Examples
$ myedenredactual import --account="My Checking" --since="2020-05-28"
```
98 changes: 98 additions & 0 deletions actual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
const { getAppConfigFromEnv } = require("./config");
const actual = require("@actual-app/api");
const fs = require("fs");
const inquirer = require("inquirer");
let { q, runQuery } = require('@actual-app/api');


const appConfig = getAppConfigFromEnv();

/**
*
* @returns {Promise<typeof actual>}
*/
async function initialize(config) {
try {
const tmp_dir = `./temp_data_actual/${config.get("user")}`
fs.mkdirSync(tmp_dir, { recursive: true });
await actual.init({
serverURL: appConfig.ACTUAL_SERVER_URL,
password: appConfig.ACTUAL_SERVER_PASSWORD,
dataDir: tmp_dir
});

let id = config.get("budget_id")
await actual.downloadBudget(id);
} catch (e) {
throw new Error(`Actual Budget Error: ${e.message}`);
}

return actual;
}

/**
*
* @param {typeof actual} actualInstance
*/
function listAccounts(actualInstance) {
return actualInstance.getAccounts();
}

/**
* Only works for the past month
* @param {typeof actual} actualInstance
* @param {*} accountId
*/
async function getLastTransactionDate(actualInstance, accountId) {
const monthAgo = new Date();
monthAgo.setMonth(monthAgo.getMonth() -3);

const transactions = await actualInstance.getTransactions(accountId, monthAgo, new Date());

if (transactions.length === 0) {
return new Date(0);
}

// Transactions of the day are already imported, so start from the next day.
const last = new Date(transactions[0].date);
last.setDate(last.getDate() + 1);

return last;
}


async function importTransactions(actualInstance, accountId, transactions) {
console.info("Importing transactions raw data START:")
console.debug(transactions)
const actualResult = await actualInstance.importTransactions(
accountId,
transactions
);
console.info("Actual logs: ", actualResult);
}

async function getBalance(actualInstance, accountId) {
const balance = await actualInstance.runQuery(q('transactions')
.filter({ account: accountId })
//.options({ splits: 'inline' })
.calculate({ $sum: '$amount' }),)
return balance.data;
}

/**
*
* @param {typeof actual} actualInstance
*/
async function finalize(actualInstance) {
await actualInstance.sync()
await actualInstance.shutdown();
}

module.exports = {
initialize,
listAccounts,
getLastTransactionDate,
importTransactions,
finalize,
getBalance
}
48 changes: 48 additions & 0 deletions cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { getAppConfigFromEnv, getConf } = require("./config.js");
const { importMyEdenredTransactions, init } = require("./engine.js");

let config;
const appConfig = getAppConfigFromEnv()

const printSyncedAccounts = () => {
const actualData = config.get("actualSync");
if (!actualData) {
console.error("No syncing data found");
return;
}

console.info("The following accounts are linked to Actual:");
console.table(
Object.values(actualData).map((account) => ({
"Actual Account": account.actualName,
"Actual Account Id": account.actualAccountId,
"MyEdenred Account Id": account.myEdenredAccountId,
}))
);
};

/**
*
* @param {string} command
* @param {object} flags
* @param {string} flags.since
*/
module.exports = async (command, flags) => {
if (!command) {
console.log('Try "thinkactual --help"');
process.exit();
}

config = getConf(flags.user || "default")

if (command === "config") {
console.log(`Config for this app is located at: ${config.path}`);
} else if (command == "init") {
init()
} else if (command === "import") {
await importMyEdenredTransactions();
} else if (command === "ls") {
printSyncedAccounts();
}
process.exit();
};
Loading

0 comments on commit 57a8273

Please sign in to comment.