Compare commits
No commits in common. "9ed0a19393474e0149c4c909af6837c295fa9925" and "01af64349e6c665bc2cf915118ec1043c16a6d54" have entirely different histories.
9ed0a19393
...
01af64349e
11
.env.example
11
.env.example
@ -2,9 +2,8 @@ ACTUAL_BUDGET_ID=your-budget-id
|
|||||||
ACTUAL_SYNC_ID=your-sync-id
|
ACTUAL_SYNC_ID=your-sync-id
|
||||||
ACTUAL_SERVER_URL=your-server-url
|
ACTUAL_SERVER_URL=your-server-url
|
||||||
ACTUAL_PASSWORD=your-password
|
ACTUAL_PASSWORD=your-password
|
||||||
# Bank
|
# Sparebank1
|
||||||
BANK_OAUTH_CLIENT_ID=your-client-id
|
SPAREBANK1_OAUTH_CLIENT_ID=your-client-id
|
||||||
BANK_OAUTH_CLIENT_SECRET=your-client-secret
|
SPAREBANK1_OAUTH_CLIENT_SECRET=your-client-secret
|
||||||
BANK_OAUTH_STATE=your-state
|
SPAREBANK1_OAUTH_STATE=your-state
|
||||||
BANK_OAUTH_REDIRECT_URI=your-redirect-uri
|
SPAREBANK1_OAUTH_REDIRECT_URI=your-redirect-uri
|
||||||
BANK_ACCOUNT_IDS=your-account-id1,your-account-id2
|
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -12,7 +12,7 @@ lerna-debug.log*
|
|||||||
|
|
||||||
# Caches
|
# Caches
|
||||||
|
|
||||||
.cache
|
data/cache/*
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
|
||||||
|
22
config.ts
22
config.ts
@ -7,21 +7,21 @@ export const ACTUAL_BUDGET_ID = getOrThrow("ACTUAL_BUDGET_ID")
|
|||||||
export const ACTUAL_SYNC_ID = getOrThrow("ACTUAL_SYNC_ID")
|
export const ACTUAL_SYNC_ID = getOrThrow("ACTUAL_SYNC_ID")
|
||||||
export const ACTUAL_SERVER_URL = getOrThrow("ACTUAL_SERVER_URL")
|
export const ACTUAL_SERVER_URL = getOrThrow("ACTUAL_SERVER_URL")
|
||||||
export const ACTUAL_PASSWORD = getOrThrow("ACTUAL_PASSWORD")
|
export const ACTUAL_PASSWORD = getOrThrow("ACTUAL_PASSWORD")
|
||||||
export const ACTUAL_ACCOUNT_IDS = getArrayOrThrow("ACTUAL_ACCOUNT_IDS")
|
export const ACTUAL_DATA_DIR = "data/cache"
|
||||||
export const ACTUAL_DATA_DIR = ".cache"
|
|
||||||
|
|
||||||
export const BANK_OAUTH_CLIENT_ID = getOrThrow("BANK_OAUTH_CLIENT_ID")
|
export const SPAREBANK1_OAUTH_CLIENT_ID = getOrThrow(
|
||||||
export const BANK_OAUTH_CLIENT_SECRET = getOrThrow("BANK_OAUTH_CLIENT_SECRET")
|
"SPAREBANK1_OAUTH_CLIENT_ID",
|
||||||
export const BANK_OAUTH_REDIRECT_URI = getOrThrow("BANK_OAUTH_REDIRECT_URI")
|
)
|
||||||
export const BANK_OAUTH_STATE = getOrThrow("BANK_OAUTH_STATE")
|
export const SPAREBANK1_OAUTH_CLIENT_SECRET = getOrThrow(
|
||||||
export const BANK_ACCOUNT_IDS = getArrayOrThrow("BANK_ACCOUNT_IDS")
|
"SPAREBANK1_OAUTH_CLIENT_SECRET",
|
||||||
|
)
|
||||||
|
export const SPAREBANK1_OAUTH_REDIRECT_URI = getOrThrow(
|
||||||
|
"SPAREBANK1_OAUTH_REDIRECT_URI",
|
||||||
|
)
|
||||||
|
export const SPAREBANK1_OAUTH_STATE = getOrThrow("SPAREBANK1_OAUTH_STATE")
|
||||||
|
|
||||||
function getOrThrow(key: string): string {
|
function getOrThrow(key: string): string {
|
||||||
const value = process.env[key]
|
const value = process.env[key]
|
||||||
assert(value, `Missing environment variable: ${key}`)
|
assert(value, `Missing environment variable: ${key}`)
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArrayOrThrow(key: string): ReadonlyArray<string> {
|
|
||||||
return getOrThrow(key).split(",")
|
|
||||||
}
|
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --import=tsx ./src/main.ts | pino-pretty",
|
"start": "node --import=tsx ./src/main.ts",
|
||||||
"test": "node --test --experimental-strip-types ./tests/**",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"format": "prettier --write \"./**/*.{js,mjs,ts,md,json}\""
|
"format": "prettier --write \"./**/*.{js,mjs,ts,md,json}\""
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@ -15,7 +15,6 @@
|
|||||||
"@actual-app/api": "^24.11.0",
|
"@actual-app/api": "^24.11.0",
|
||||||
"cron": "^3.2.1",
|
"cron": "^3.2.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"pino": "^9.5.0",
|
|
||||||
"prettier": "^3.3.3"
|
"prettier": "^3.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
93
pnpm-lock.yaml
generated
93
pnpm-lock.yaml
generated
@ -17,9 +17,6 @@ importers:
|
|||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.4.5
|
specifier: ^16.4.5
|
||||||
version: 16.4.5
|
version: 16.4.5
|
||||||
pino:
|
|
||||||
specifier: ^9.5.0
|
|
||||||
version: 9.5.0
|
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.3.3
|
specifier: ^3.3.3
|
||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
@ -193,10 +190,6 @@ packages:
|
|||||||
'@types/node@22.9.0':
|
'@types/node@22.9.0':
|
||||||
resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==}
|
resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==}
|
||||||
|
|
||||||
atomic-sleep@1.0.0:
|
|
||||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
|
||||||
engines: {node: '>=8.0.0'}
|
|
||||||
|
|
||||||
base64-js@1.5.1:
|
base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
@ -253,10 +246,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
fast-redact@3.5.0:
|
|
||||||
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
fetch-blob@3.2.0:
|
fetch-blob@3.2.0:
|
||||||
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||||
engines: {node: ^12.20 || >= 14.13}
|
engines: {node: ^12.20 || >= 14.13}
|
||||||
@ -326,23 +315,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
on-exit-leak-free@2.1.2:
|
|
||||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
|
|
||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
pino-abstract-transport@2.0.0:
|
|
||||||
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
|
|
||||||
|
|
||||||
pino-std-serializers@7.0.0:
|
|
||||||
resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
|
|
||||||
|
|
||||||
pino@9.5.0:
|
|
||||||
resolution: {integrity: sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
prebuild-install@7.1.2:
|
prebuild-install@7.1.2:
|
||||||
resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==}
|
resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -353,15 +328,9 @@ packages:
|
|||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
process-warning@4.0.0:
|
|
||||||
resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==}
|
|
||||||
|
|
||||||
pump@3.0.2:
|
pump@3.0.2:
|
||||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4:
|
|
||||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
|
||||||
|
|
||||||
rc@1.2.8:
|
rc@1.2.8:
|
||||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -370,20 +339,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
real-require@0.2.0:
|
|
||||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
|
||||||
engines: {node: '>= 12.13.0'}
|
|
||||||
|
|
||||||
resolve-pkg-maps@1.0.0:
|
resolve-pkg-maps@1.0.0:
|
||||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
safe-buffer@5.2.1:
|
safe-buffer@5.2.1:
|
||||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
safe-stable-stringify@2.5.0:
|
|
||||||
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
semver@7.6.3:
|
semver@7.6.3:
|
||||||
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
|
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -395,13 +356,6 @@ packages:
|
|||||||
simple-get@4.0.1:
|
simple-get@4.0.1:
|
||||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||||
|
|
||||||
sonic-boom@4.2.0:
|
|
||||||
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
|
|
||||||
|
|
||||||
split2@4.2.0:
|
|
||||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
|
||||||
engines: {node: '>= 10.x'}
|
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
string_decoder@1.3.0:
|
||||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
|
|
||||||
@ -416,9 +370,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
thread-stream@3.1.0:
|
|
||||||
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
|
||||||
|
|
||||||
tsx@4.19.2:
|
tsx@4.19.2:
|
||||||
resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==}
|
resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@ -543,8 +494,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.19.8
|
undici-types: 6.19.8
|
||||||
|
|
||||||
atomic-sleep@1.0.0: {}
|
|
||||||
|
|
||||||
base64-js@1.5.1: {}
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
better-sqlite3@9.6.0:
|
better-sqlite3@9.6.0:
|
||||||
@ -621,8 +570,6 @@ snapshots:
|
|||||||
|
|
||||||
expand-template@2.0.3: {}
|
expand-template@2.0.3: {}
|
||||||
|
|
||||||
fast-redact@3.5.0: {}
|
|
||||||
|
|
||||||
fetch-blob@3.2.0:
|
fetch-blob@3.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
node-domexception: 1.0.0
|
node-domexception: 1.0.0
|
||||||
@ -677,32 +624,10 @@ snapshots:
|
|||||||
fetch-blob: 3.2.0
|
fetch-blob: 3.2.0
|
||||||
formdata-polyfill: 4.0.10
|
formdata-polyfill: 4.0.10
|
||||||
|
|
||||||
on-exit-leak-free@2.1.2: {}
|
|
||||||
|
|
||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
|
|
||||||
pino-abstract-transport@2.0.0:
|
|
||||||
dependencies:
|
|
||||||
split2: 4.2.0
|
|
||||||
|
|
||||||
pino-std-serializers@7.0.0: {}
|
|
||||||
|
|
||||||
pino@9.5.0:
|
|
||||||
dependencies:
|
|
||||||
atomic-sleep: 1.0.0
|
|
||||||
fast-redact: 3.5.0
|
|
||||||
on-exit-leak-free: 2.1.2
|
|
||||||
pino-abstract-transport: 2.0.0
|
|
||||||
pino-std-serializers: 7.0.0
|
|
||||||
process-warning: 4.0.0
|
|
||||||
quick-format-unescaped: 4.0.4
|
|
||||||
real-require: 0.2.0
|
|
||||||
safe-stable-stringify: 2.5.0
|
|
||||||
sonic-boom: 4.2.0
|
|
||||||
thread-stream: 3.1.0
|
|
||||||
|
|
||||||
prebuild-install@7.1.2:
|
prebuild-install@7.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-libc: 2.0.3
|
detect-libc: 2.0.3
|
||||||
@ -720,15 +645,11 @@ snapshots:
|
|||||||
|
|
||||||
prettier@3.3.3: {}
|
prettier@3.3.3: {}
|
||||||
|
|
||||||
process-warning@4.0.0: {}
|
|
||||||
|
|
||||||
pump@3.0.2:
|
pump@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
end-of-stream: 1.4.4
|
end-of-stream: 1.4.4
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4: {}
|
|
||||||
|
|
||||||
rc@1.2.8:
|
rc@1.2.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-extend: 0.6.0
|
deep-extend: 0.6.0
|
||||||
@ -742,14 +663,10 @@ snapshots:
|
|||||||
string_decoder: 1.3.0
|
string_decoder: 1.3.0
|
||||||
util-deprecate: 1.0.2
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
real-require@0.2.0: {}
|
|
||||||
|
|
||||||
resolve-pkg-maps@1.0.0: {}
|
resolve-pkg-maps@1.0.0: {}
|
||||||
|
|
||||||
safe-buffer@5.2.1: {}
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
safe-stable-stringify@2.5.0: {}
|
|
||||||
|
|
||||||
semver@7.6.3: {}
|
semver@7.6.3: {}
|
||||||
|
|
||||||
simple-concat@1.0.1: {}
|
simple-concat@1.0.1: {}
|
||||||
@ -760,12 +677,6 @@ snapshots:
|
|||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
simple-concat: 1.0.1
|
simple-concat: 1.0.1
|
||||||
|
|
||||||
sonic-boom@4.2.0:
|
|
||||||
dependencies:
|
|
||||||
atomic-sleep: 1.0.0
|
|
||||||
|
|
||||||
split2@4.2.0: {}
|
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
string_decoder@1.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
@ -787,10 +698,6 @@ snapshots:
|
|||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
thread-stream@3.1.0:
|
|
||||||
dependencies:
|
|
||||||
real-require: 0.2.0
|
|
||||||
|
|
||||||
tsx@4.19.2:
|
tsx@4.19.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.23.1
|
esbuild: 0.23.1
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
ACTUAL_DATA_DIR,
|
ACTUAL_DATA_DIR,
|
||||||
ACTUAL_PASSWORD,
|
ACTUAL_PASSWORD,
|
||||||
ACTUAL_SERVER_URL,
|
ACTUAL_SERVER_URL,
|
||||||
|
ACTUAL_SYNC_ID,
|
||||||
} from "../config.ts"
|
} from "../config.ts"
|
||||||
import type { TransactionEntity } from "@actual-app/api/@types/loot-core/types/models"
|
import type { TransactionEntity } from "@actual-app/api/@types/loot-core/types/models"
|
||||||
import { type UUID } from "node:crypto"
|
import { type UUID } from "node:crypto"
|
||||||
@ -52,3 +53,28 @@ export class ActualImpl implements Actual {
|
|||||||
return await actual.shutdown()
|
return await actual.shutdown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function init() {
|
||||||
|
return await actual.init({
|
||||||
|
// Budget data will be cached locally here, in subdirectories for each file.
|
||||||
|
dataDir: ACTUAL_DATA_DIR,
|
||||||
|
// This is the URL of your running server
|
||||||
|
serverURL: ACTUAL_SERVER_URL,
|
||||||
|
// This is the password you use to log into the server
|
||||||
|
password: ACTUAL_PASSWORD,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadBudget() {
|
||||||
|
const something = await actual.downloadBudget(ACTUAL_SYNC_ID)
|
||||||
|
console.log("downloadBudget", something)
|
||||||
|
return something
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAccounts() {
|
||||||
|
return await actual.getAccounts()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function shutdown() {
|
||||||
|
await actual.shutdown()
|
||||||
|
}
|
||||||
|
47
src/main.ts
47
src/main.ts
@ -1,53 +1,32 @@
|
|||||||
import { type Actual, ActualImpl } from "@/actual.ts"
|
import { type Actual, ActualImpl } from "@/actual.ts"
|
||||||
import { cronJobDaily } from "@/cron.ts"
|
import { cronJobDaily } from "@/cron.ts"
|
||||||
import { type Bank, Sparebank1Impl, type Transaction } from "@/sparebank1.ts"
|
import { type Bank, Sparebank1Impl } from "@/sparebank1.ts"
|
||||||
import { bankTransactionIntoActualTransaction } from "@/mappings.ts"
|
import { transactionIntoActualTransaction } from "@/mappings.ts"
|
||||||
import { ACTUAL_ACCOUNT_IDS, BANK_ACCOUNT_IDS } from "../config.ts"
|
|
||||||
import logger from "pino"
|
|
||||||
import type { UUID } from "node:crypto"
|
|
||||||
|
|
||||||
// TODO Transports api for pino https://github.com/pinojs/pino/blob/HEAD/docs/transports.md
|
async function daily(actual: Actual, bank: Bank): Promise<() => Promise<void>> {
|
||||||
// TODO create .cache if missing
|
return async () => {
|
||||||
|
console.log("Wake up! It's 1 AM!")
|
||||||
export async function daily(actual: Actual, bank: Bank): Promise<void> {
|
|
||||||
// Fetch transactions from the bank
|
// Fetch transactions from the bank
|
||||||
const transactions = await fetchTransactionsFromPastDay(bank)
|
const transactions = await bank.transactionsPastDay(
|
||||||
logger().info(`Fetched ${transactions.length} transactions`)
|
"my_account",
|
||||||
|
"my_access_token",
|
||||||
|
)
|
||||||
|
|
||||||
// TODO multiple accounts
|
// TODO account? id or name?
|
||||||
const accountId = ACTUAL_ACCOUNT_IDS[0] as UUID
|
|
||||||
const actualTransactions = transactions.map((transaction) =>
|
const actualTransactions = transactions.map((transaction) =>
|
||||||
bankTransactionIntoActualTransaction(transaction, accountId),
|
transactionIntoActualTransaction(transaction, ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO Import transactions into Actual
|
// TODO Import transactions into Actual
|
||||||
// If multiple accounts, loop over them
|
// If multiple accounts, loop over them
|
||||||
// Get account ID from mapper
|
// Get account ID from mapper
|
||||||
|
await actual.importTransactions("a-b-c-d-e", actualTransactions)
|
||||||
// TODO TypeError: Cannot read properties of undefined (reading 'timestamp')
|
|
||||||
await actual.importTransactions(accountId, actualTransactions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchTransactionsFromPastDay(
|
|
||||||
bank: Bank,
|
|
||||||
): Promise<ReadonlyArray<Transaction>> {
|
|
||||||
// TODO refresh token
|
|
||||||
const { access_token } = await bank.refreshToken("my_refresh_token")
|
|
||||||
return bank.transactionsPastDay(BANK_ACCOUNT_IDS, access_token)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
logger().info("Starting application")
|
|
||||||
const actual = await ActualImpl.init()
|
const actual = await ActualImpl.init()
|
||||||
logger().info("Initialized Actual Budget API")
|
cronJobDaily(await daily(actual, new Sparebank1Impl()))
|
||||||
|
|
||||||
cronJobDaily(async () => {
|
|
||||||
logger().info("Running daily job")
|
|
||||||
await daily(actual, new Sparebank1Impl())
|
|
||||||
logger().info("Finished daily job")
|
|
||||||
})
|
|
||||||
|
|
||||||
// logger().info("Shutting down")
|
|
||||||
// await actual.shutdown()
|
// await actual.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
import type { Transaction } from "@/sparebank1.ts"
|
import type { Transaction } from "@/sparebank1.ts"
|
||||||
import type { TransactionEntity } from "@actual-app/api/@types/loot-core/types/models"
|
import type { TransactionEntity } from "@actual-app/api/@types/loot-core/types/models"
|
||||||
import type { UUID } from "node:crypto"
|
|
||||||
|
|
||||||
// TODO more fields / correct fields?
|
// TODO more fields / correct fields?
|
||||||
export function bankTransactionIntoActualTransaction(
|
export function transactionIntoActualTransaction(
|
||||||
transaction: Transaction,
|
transaction: Transaction,
|
||||||
accountId: UUID,
|
account: string,
|
||||||
): TransactionEntity {
|
): TransactionEntity {
|
||||||
return {
|
return {
|
||||||
id: transaction.id,
|
id: transaction.id,
|
||||||
// Transactions with the same id will be ignored
|
account,
|
||||||
imported_id: transaction.id,
|
amount: transaction.amount,
|
||||||
account: accountId,
|
|
||||||
// The value without decimals
|
|
||||||
amount: transaction.amount * 100,
|
|
||||||
date: transaction.date,
|
date: transaction.date,
|
||||||
payee: transaction.description,
|
payee: transaction.description,
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
import {
|
||||||
|
SPAREBANK1_OAUTH_CLIENT_ID,
|
||||||
|
SPAREBANK1_OAUTH_REDIRECT_URI,
|
||||||
|
SPAREBANK1_OAUTH_STATE,
|
||||||
|
} from "../config.ts"
|
||||||
|
|
||||||
// TODO move types
|
// TODO move types
|
||||||
export interface OAuthTokenResponse {
|
export interface OAuthTokenResponse {
|
||||||
access_token: string
|
access_token: string
|
||||||
@ -34,14 +40,18 @@ export interface Sparebank1 {
|
|||||||
export class Sparebank1Impl implements Sparebank1 {
|
export class Sparebank1Impl implements Sparebank1 {
|
||||||
private baseUrl = "https://api.sparebank1.no"
|
private baseUrl = "https://api.sparebank1.no"
|
||||||
|
|
||||||
// TODO remove?
|
|
||||||
async accessToken(): Promise<OAuthTokenResponse> {
|
async accessToken(): Promise<OAuthTokenResponse> {
|
||||||
throw new Error("Not implemented")
|
const response = await fetch(`${this.baseUrl}/oauth/authorize?
|
||||||
|
client_id=${SPAREBANK1_OAUTH_CLIENT_ID}&
|
||||||
|
state=${SPAREBANK1_OAUTH_STATE}&
|
||||||
|
redirect_uri=${SPAREBANK1_OAUTH_REDIRECT_URI}&
|
||||||
|
finInst=fid-smn&
|
||||||
|
response_type=code`)
|
||||||
|
|
||||||
// if (response.ok) {
|
if (response.ok) {
|
||||||
// return await response.json()
|
return await response.json()
|
||||||
// }
|
}
|
||||||
// throw new Error(`Failed to get access token. ${response.statusText}`)
|
throw new Error(`Failed to get access token. ${response.statusText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshToken(refreshToken: string): Promise<OAuthTokenResponse> {
|
async refreshToken(refreshToken: string): Promise<OAuthTokenResponse> {
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { describe, it } from "node:test"
|
|
||||||
import { daily } from "@/main.ts"
|
|
||||||
import { ActualImpl } from "@/actual.ts"
|
|
||||||
import { BankStub } from "./stubs/bankStub.ts"
|
|
||||||
import assert from "node:assert"
|
|
||||||
|
|
||||||
describe("Main logic of the application", () => {
|
|
||||||
it("should import the transactions to Actual Budget", async () => {
|
|
||||||
await daily(await ActualImpl.init(), new BankStub())
|
|
||||||
assert.ok(true)
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,49 +0,0 @@
|
|||||||
import type { Bank, OAuthTokenResponse, Transaction } from "@/sparebank1.ts"
|
|
||||||
|
|
||||||
const tokenResponse: OAuthTokenResponse = {
|
|
||||||
access_token: "my_access_token",
|
|
||||||
token_type: "Bearer",
|
|
||||||
expires_in: 3600,
|
|
||||||
refresh_token: "my_refresh_token",
|
|
||||||
refresh_token_expires_in: 3600,
|
|
||||||
refresh_token_absolute_expires_in: 3600,
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BankStub implements Bank {
|
|
||||||
async accessToken(): Promise<OAuthTokenResponse> {
|
|
||||||
return tokenResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshToken(_unused: string): Promise<OAuthTokenResponse> {
|
|
||||||
return tokenResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
async transactionsPastDay(
|
|
||||||
_accountIds: ReadonlyArray<string> | string,
|
|
||||||
_accessToken: string,
|
|
||||||
): Promise<ReadonlyArray<Transaction>> {
|
|
||||||
const someFields = {
|
|
||||||
date: new Date().toDateString(),
|
|
||||||
description: "Test transaction",
|
|
||||||
cleanedDescription: "Test transaction",
|
|
||||||
remoteAccountName: "Test account",
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
amount: 100,
|
|
||||||
...someFields,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
amount: 200,
|
|
||||||
...someFields,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
amount: -50,
|
|
||||||
...someFields,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user