- Added Actual account ids and log level to .env.example.
- Fixed timestamp error by downloading the budget after init.
- Hardcoded date in test.
- Separate logging file for configurations.
- Organized test
- More logging in main
- Fixed wrong paths for some files
- Load a .env.test.local file when running tests

Signed-off-by: Martin Berg Alstad <git@martials.no>
This commit is contained in:
Martin Berg Alstad 2024-12-23 16:47:07 +01:00
parent 29b394baf4
commit 480c0356f9
Signed by: martials
GPG Key ID: C4EA170D0B21376A
9 changed files with 144 additions and 33 deletions

View File

@ -2,9 +2,13 @@ ACTUAL_BUDGET_ID=your-budget-id
ACTUAL_SYNC_ID=your-sync-id
ACTUAL_SERVER_URL=your-server-url
ACTUAL_PASSWORD=your-password
ACTUAL_ACCOUNT_IDS=your-account-id1,your-account-id2
# Bank
BANK_OAUTH_CLIENT_ID=your-client-id
BANK_OAUTH_CLIENT_SECRET=your-client-secret
BANK_OAUTH_STATE=your-state
BANK_OAUTH_REDIRECT_URI=your-redirect-uri
BANK_ACCOUNT_IDS=your-account-id1,your-account-id2
BANK_ACCOUNT_IDS=your-account-id1,your-account-id2
# Configuration
# trace | error | warn | info | debug | trace
LOG_LEVEL=info

View File

@ -5,7 +5,7 @@
"main": "index.js",
"scripts": {
"start": "node --import=tsx ./src/main.ts | pino-pretty",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test": "dotenvx run --env-file=.env.test.local -- node --experimental-vm-modules node_modules/jest/bin/jest.js | pino-pretty",
"format": "prettier --write \"./**/*.{js,mjs,ts,md,json}\""
},
"keywords": [],
@ -13,6 +13,7 @@
"license": "ISC",
"dependencies": {
"@actual-app/api": "^24.12.0",
"@dotenvx/dotenvx": "^1.31.3",
"cron": "^3.3.1",
"dotenv": "^16.4.7",
"pino": "^9.5.0",

119
pnpm-lock.yaml generated
View File

@ -8,6 +8,9 @@ dependencies:
'@actual-app/api':
specifier: ^24.12.0
version: 24.12.0
'@dotenvx/dotenvx':
specifier: ^1.31.3
version: 1.31.3
cron:
specifier: ^3.3.1
version: 3.3.1
@ -397,6 +400,30 @@ packages:
'@jridgewell/trace-mapping': 0.3.9
dev: true
/@dotenvx/dotenvx@1.31.3:
resolution: {integrity: sha512-NgRjBV8NrCIoRhdbPozkKp+HvSn0Sc8DrOT22YDvTbs5pgPC2YrXKqwI7YwLFDVHBjSJHJTvkhQ5QHCCO+//yg==}
hasBin: true
dependencies:
commander: 11.1.0
dotenv: 16.4.7
eciesjs: 0.4.13
execa: 5.1.1
fdir: 6.4.2(picomatch@4.0.2)
ignore: 5.3.2
object-treeify: 1.1.33
picomatch: 4.0.2
which: 4.0.0
dev: false
/@ecies/ciphers@0.2.2(@noble/ciphers@1.1.3):
resolution: {integrity: sha512-ylfGR7PyTd+Rm2PqQowG08BCKA22QuX8NzrL+LxAAvazN10DMwdJ2fWwAzRj05FI/M8vNFGm3cv9Wq/GFWCBLg==}
engines: {bun: '>=1', deno: '>=2', node: '>=16'}
peerDependencies:
'@noble/ciphers': ^1.0.0
dependencies:
'@noble/ciphers': 1.1.3
dev: false
/@esbuild/aix-ppc64@0.23.1:
resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==}
engines: {node: '>=18'}
@ -880,6 +907,28 @@ packages:
'@jridgewell/sourcemap-codec': 1.5.0
dev: true
/@noble/ciphers@1.1.3:
resolution: {integrity: sha512-Ygv6WnWJHLLiW4fnNDC1z+i13bud+enXOFRBlpxI+NJliPWx5wdR+oWlTjLuBPTqjUjtHXtjkU6w3kuuH6upZA==}
engines: {node: ^14.21.3 || >=16}
dev: false
/@noble/curves@1.7.0:
resolution: {integrity: sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==}
engines: {node: ^14.21.3 || >=16}
dependencies:
'@noble/hashes': 1.6.0
dev: false
/@noble/hashes@1.6.0:
resolution: {integrity: sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==}
engines: {node: ^14.21.3 || >=16}
dev: false
/@noble/hashes@1.6.1:
resolution: {integrity: sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==}
engines: {node: ^14.21.3 || >=16}
dev: false
/@sinclair/typebox@0.27.8:
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
dev: true
@ -1292,6 +1341,11 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true
/commander@11.1.0:
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
engines: {node: '>=16'}
dev: false
/compare-versions@6.1.1:
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
dev: false
@ -1341,7 +1395,6 @@ packages:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
dev: true
/data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
@ -1411,6 +1464,16 @@ packages:
engines: {node: '>=12'}
dev: false
/eciesjs@0.4.13:
resolution: {integrity: sha512-zBdtR4K+wbj10bWPpIOF9DW+eFYQu8miU5ypunh0t4Bvt83ZPlEWgT5Dq/0G6uwEXumZKjfb5BZxYUZQ2Hzn/Q==}
engines: {bun: '>=1', deno: '>=2', node: '>=16'}
dependencies:
'@ecies/ciphers': 0.2.2(@noble/ciphers@1.1.3)
'@noble/ciphers': 1.1.3
'@noble/curves': 1.7.0
'@noble/hashes': 1.6.1
dev: false
/ejs@3.1.10:
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
engines: {node: '>=0.10.0'}
@ -1505,7 +1568,6 @@ packages:
onetime: 5.1.2
signal-exit: 3.0.7
strip-final-newline: 2.0.0
dev: true
/exit@0.1.2:
resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==}
@ -1543,6 +1605,17 @@ packages:
bser: 2.1.1
dev: true
/fdir@6.4.2(picomatch@4.0.2):
resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
dependencies:
picomatch: 4.0.2
dev: false
/fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
@ -1621,7 +1694,6 @@ packages:
/get-stream@6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
dev: true
/get-tsconfig@4.8.1:
resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==}
@ -1677,12 +1749,16 @@ packages:
/human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
dev: true
/ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: false
/ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
dev: false
/import-local@3.2.0:
resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==}
engines: {node: '>=8'}
@ -1741,11 +1817,14 @@ packages:
/is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
dev: true
/isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
/isexe@3.1.1:
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
engines: {node: '>=16'}
dev: false
/istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
@ -2311,7 +2390,6 @@ packages:
/merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: true
/micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
@ -2324,7 +2402,6 @@ packages:
/mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
dev: true
/mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
@ -2407,7 +2484,11 @@ packages:
engines: {node: '>=8'}
dependencies:
path-key: 3.1.1
dev: true
/object-treeify@1.1.33:
resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==}
engines: {node: '>= 10'}
dev: false
/on-exit-leak-free@2.1.2:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
@ -2424,7 +2505,6 @@ packages:
engines: {node: '>=6'}
dependencies:
mimic-fn: 2.1.0
dev: true
/p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
@ -2475,7 +2555,6 @@ packages:
/path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
dev: true
/path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@ -2490,6 +2569,11 @@ packages:
engines: {node: '>=8.6'}
dev: true
/picomatch@4.0.2:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
dev: false
/pino-abstract-transport@2.0.0:
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
dependencies:
@ -2678,16 +2762,13 @@ packages:
engines: {node: '>=8'}
dependencies:
shebang-regex: 3.0.0
dev: true
/shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
dev: true
/signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: true
/simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
@ -2782,7 +2863,6 @@ packages:
/strip-final-newline@2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
dev: true
/strip-json-comments@2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
@ -3015,7 +3095,14 @@ packages:
hasBin: true
dependencies:
isexe: 2.0.0
dev: true
/which@4.0.0:
resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==}
engines: {node: ^16.13.0 || >=18.0.0}
hasBin: true
dependencies:
isexe: 3.1.1
dev: false
/wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}

View File

@ -3,9 +3,11 @@ import {
ACTUAL_DATA_DIR,
ACTUAL_PASSWORD,
ACTUAL_SERVER_URL,
ACTUAL_SYNC_ID,
} from "../config.ts"
import type { TransactionEntity } from "@actual-app/api/@types/loot-core/types/models"
import { type UUID } from "node:crypto"
import logger from "@/logger.ts"
export interface Actual {
importTransactions: (
@ -38,6 +40,9 @@ export class ActualImpl implements Actual {
// This is the password you use to log into the server
password: ACTUAL_PASSWORD,
})
logger.info(`Initialized ActualBudget API for ${ACTUAL_SERVER_URL}`)
await actual.downloadBudget(ACTUAL_SYNC_ID)
logger.info(`Downloaded budget`)
return new ActualImpl()
}

8
src/logger.ts Normal file
View File

@ -0,0 +1,8 @@
import pino from "pino"
/**
* / Returns a logging instance with the default log-level "info"
*/
export default pino({
level: process.env.LOG_LEVEL as string || "info",
})

View File

@ -7,7 +7,7 @@ import {
} from "@/bank/sparebank1.ts"
import { bankTransactionIntoActualTransaction } from "@/mappings.ts"
import { ACTUAL_ACCOUNT_IDS, BANK_ACCOUNT_IDS } from "../config.ts"
import logger from "pino"
import logger from "./logger.ts"
import type { UUID } from "node:crypto"
// TODO Transports api for pino https://github.com/pinojs/pino/blob/HEAD/docs/transports.md
@ -16,7 +16,7 @@ import type { UUID } from "node:crypto"
export async function daily(actual: Actual, bank: Bank): Promise<void> {
// Fetch transactions from the bank
const transactions = await fetchTransactionsFromPastDay(bank)
logger().info(`Fetched ${transactions.length} transactions`)
logger.info(`Fetched ${transactions.length} transactions`)
// TODO multiple accounts
const accountId = ACTUAL_ACCOUNT_IDS[0] as UUID
@ -24,12 +24,14 @@ export async function daily(actual: Actual, bank: Bank): Promise<void> {
bankTransactionIntoActualTransaction(transaction, accountId),
)
logger.debug(`Mapped ${JSON.stringify(transactions)} to ${JSON.stringify(actualTransactions)} transactions`)
// TODO Import transactions into Actual
// If multiple accounts, loop over them
// Get account ID from mapper
// TODO TypeError: Cannot read properties of undefined (reading 'timestamp')
await actual.importTransactions(accountId, actualTransactions)
const response = await actual.importTransactions(accountId, actualTransactions)
logger.info(`ImportTransactionsResponse=${JSON.stringify(response)}`)
}
async function fetchTransactionsFromPastDay(
@ -41,17 +43,17 @@ async function fetchTransactionsFromPastDay(
}
async function main(): Promise<void> {
logger().info("Starting application")
logger.info("Starting application")
const actual = await ActualImpl.init()
logger().info("Initialized Actual Budget API")
logger.info("Waiting for CRON job to start")
cronJobDaily(async () => {
logger().info("Running daily job")
logger.info("Running daily job")
await daily(actual, new Sparebank1Impl())
logger().info("Finished daily job")
logger.info("Finished daily job")
})
// logger().info("Shutting down")
// logger.info("Shutting down")
// await actual.shutdown()
}

View File

@ -1,4 +1,4 @@
import type { Transaction } from "@/sparebank1.ts"
import type { Transaction } from "@/bank/sparebank1.ts"
import type { TransactionEntity } from "@actual-app/api/@types/loot-core/types/models"
import type { UUID } from "node:crypto"

View File

@ -1,12 +1,16 @@
import { describe, expect, it } from "@jest/globals"
import { describe, it } from "@jest/globals"
import { daily } from "@/main.ts"
import { ActualImpl } from "@/actual.ts"
import { BankStub } from "./stubs/bankStub.ts"
// TODO testcontainers with Actual?
// TODO tests don't stop after completing
describe("Main logic of the application", () => {
it("should import the transactions to Actual Budget", async () => {
await daily(await ActualImpl.init(), new BankStub())
expect(true)
const actual = await ActualImpl.init()
await daily(actual, new BankStub())
await actual.shutdown()
})
})

View File

@ -1,4 +1,4 @@
import type { Bank, OAuthTokenResponse, Transaction } from "@/sparebank1.ts"
import type { Bank, OAuthTokenResponse, Transaction } from "@/bank/sparebank1.ts"
const tokenResponse: OAuthTokenResponse = {
access_token: "my_access_token",
@ -23,7 +23,7 @@ export class BankStub implements Bank {
_accessToken: string,
): Promise<ReadonlyArray<Transaction>> {
const someFields = {
date: new Date().toDateString(),
date: "2019-08-20",
description: "Test transaction",
cleanedDescription: "Test transaction",
remoteAccountName: "Test account",