Compare commits
2 Commits
01af64349e
...
9ed0a19393
Author | SHA1 | Date | |
---|---|---|---|
9ed0a19393 | |||
cc325b9f08 |
11
.env.example
11
.env.example
@ -2,8 +2,9 @@ ACTUAL_BUDGET_ID=your-budget-id
|
||||
ACTUAL_SYNC_ID=your-sync-id
|
||||
ACTUAL_SERVER_URL=your-server-url
|
||||
ACTUAL_PASSWORD=your-password
|
||||
# Sparebank1
|
||||
SPAREBANK1_OAUTH_CLIENT_ID=your-client-id
|
||||
SPAREBANK1_OAUTH_CLIENT_SECRET=your-client-secret
|
||||
SPAREBANK1_OAUTH_STATE=your-state
|
||||
SPAREBANK1_OAUTH_REDIRECT_URI=your-redirect-uri
|
||||
# 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
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -12,7 +12,7 @@ lerna-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
data/cache/*
|
||||
.cache
|
||||
|
||||
# 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_SERVER_URL = getOrThrow("ACTUAL_SERVER_URL")
|
||||
export const ACTUAL_PASSWORD = getOrThrow("ACTUAL_PASSWORD")
|
||||
export const ACTUAL_DATA_DIR = "data/cache"
|
||||
export const ACTUAL_ACCOUNT_IDS = getArrayOrThrow("ACTUAL_ACCOUNT_IDS")
|
||||
export const ACTUAL_DATA_DIR = ".cache"
|
||||
|
||||
export const SPAREBANK1_OAUTH_CLIENT_ID = getOrThrow(
|
||||
"SPAREBANK1_OAUTH_CLIENT_ID",
|
||||
)
|
||||
export const SPAREBANK1_OAUTH_CLIENT_SECRET = getOrThrow(
|
||||
"SPAREBANK1_OAUTH_CLIENT_SECRET",
|
||||
)
|
||||
export const SPAREBANK1_OAUTH_REDIRECT_URI = getOrThrow(
|
||||
"SPAREBANK1_OAUTH_REDIRECT_URI",
|
||||
)
|
||||
export const SPAREBANK1_OAUTH_STATE = getOrThrow("SPAREBANK1_OAUTH_STATE")
|
||||
export const BANK_OAUTH_CLIENT_ID = getOrThrow("BANK_OAUTH_CLIENT_ID")
|
||||
export const BANK_OAUTH_CLIENT_SECRET = getOrThrow("BANK_OAUTH_CLIENT_SECRET")
|
||||
export const BANK_OAUTH_REDIRECT_URI = getOrThrow("BANK_OAUTH_REDIRECT_URI")
|
||||
export const BANK_OAUTH_STATE = getOrThrow("BANK_OAUTH_STATE")
|
||||
export const BANK_ACCOUNT_IDS = getArrayOrThrow("BANK_ACCOUNT_IDS")
|
||||
|
||||
function getOrThrow(key: string): string {
|
||||
const value = process.env[key]
|
||||
assert(value, `Missing environment variable: ${key}`)
|
||||
return value
|
||||
}
|
||||
|
||||
function getArrayOrThrow(key: string): ReadonlyArray<string> {
|
||||
return getOrThrow(key).split(",")
|
||||
}
|
||||
|
@ -4,8 +4,8 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node --import=tsx ./src/main.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node --import=tsx ./src/main.ts | pino-pretty",
|
||||
"test": "node --test --experimental-strip-types ./tests/**",
|
||||
"format": "prettier --write \"./**/*.{js,mjs,ts,md,json}\""
|
||||
},
|
||||
"keywords": [],
|
||||
@ -15,6 +15,7 @@
|
||||
"@actual-app/api": "^24.11.0",
|
||||
"cron": "^3.2.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"pino": "^9.5.0",
|
||||
"prettier": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
93
pnpm-lock.yaml
generated
93
pnpm-lock.yaml
generated
@ -17,6 +17,9 @@ importers:
|
||||
dotenv:
|
||||
specifier: ^16.4.5
|
||||
version: 16.4.5
|
||||
pino:
|
||||
specifier: ^9.5.0
|
||||
version: 9.5.0
|
||||
prettier:
|
||||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
@ -190,6 +193,10 @@ packages:
|
||||
'@types/node@22.9.0':
|
||||
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:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
@ -246,6 +253,10 @@ packages:
|
||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
fast-redact@3.5.0:
|
||||
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||
engines: {node: ^12.20 || >= 14.13}
|
||||
@ -315,9 +326,23 @@ packages:
|
||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||
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:
|
||||
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:
|
||||
resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -328,9 +353,15 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
process-warning@4.0.0:
|
||||
resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==}
|
||||
|
||||
pump@3.0.2:
|
||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||
|
||||
quick-format-unescaped@4.0.4:
|
||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||
|
||||
rc@1.2.8:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
@ -339,12 +370,20 @@ packages:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
|
||||
engines: {node: '>=10'}
|
||||
@ -356,6 +395,13 @@ packages:
|
||||
simple-get@4.0.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
@ -370,6 +416,9 @@ packages:
|
||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
thread-stream@3.1.0:
|
||||
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||
|
||||
tsx@4.19.2:
|
||||
resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@ -494,6 +543,8 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
atomic-sleep@1.0.0: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
better-sqlite3@9.6.0:
|
||||
@ -570,6 +621,8 @@ snapshots:
|
||||
|
||||
expand-template@2.0.3: {}
|
||||
|
||||
fast-redact@3.5.0: {}
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
@ -624,10 +677,32 @@ snapshots:
|
||||
fetch-blob: 3.2.0
|
||||
formdata-polyfill: 4.0.10
|
||||
|
||||
on-exit-leak-free@2.1.2: {}
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
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:
|
||||
dependencies:
|
||||
detect-libc: 2.0.3
|
||||
@ -645,11 +720,15 @@ snapshots:
|
||||
|
||||
prettier@3.3.3: {}
|
||||
|
||||
process-warning@4.0.0: {}
|
||||
|
||||
pump@3.0.2:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.4
|
||||
once: 1.4.0
|
||||
|
||||
quick-format-unescaped@4.0.4: {}
|
||||
|
||||
rc@1.2.8:
|
||||
dependencies:
|
||||
deep-extend: 0.6.0
|
||||
@ -663,10 +742,14 @@ snapshots:
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
real-require@0.2.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
safe-stable-stringify@2.5.0: {}
|
||||
|
||||
semver@7.6.3: {}
|
||||
|
||||
simple-concat@1.0.1: {}
|
||||
@ -677,6 +760,12 @@ snapshots:
|
||||
once: 1.4.0
|
||||
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:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
@ -698,6 +787,10 @@ snapshots:
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
thread-stream@3.1.0:
|
||||
dependencies:
|
||||
real-require: 0.2.0
|
||||
|
||||
tsx@4.19.2:
|
||||
dependencies:
|
||||
esbuild: 0.23.1
|
||||
|
@ -3,7 +3,6 @@ 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"
|
||||
@ -53,28 +52,3 @@ export class ActualImpl implements Actual {
|
||||
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,32 +1,53 @@
|
||||
import { type Actual, ActualImpl } from "@/actual.ts"
|
||||
import { cronJobDaily } from "@/cron.ts"
|
||||
import { type Bank, Sparebank1Impl } from "@/sparebank1.ts"
|
||||
import { transactionIntoActualTransaction } from "@/mappings.ts"
|
||||
import { type Bank, Sparebank1Impl, type Transaction } from "@/sparebank1.ts"
|
||||
import { bankTransactionIntoActualTransaction } from "@/mappings.ts"
|
||||
import { ACTUAL_ACCOUNT_IDS, BANK_ACCOUNT_IDS } from "../config.ts"
|
||||
import logger from "pino"
|
||||
import type { UUID } from "node:crypto"
|
||||
|
||||
async function daily(actual: Actual, bank: Bank): Promise<() => Promise<void>> {
|
||||
return async () => {
|
||||
console.log("Wake up! It's 1 AM!")
|
||||
// TODO Transports api for pino https://github.com/pinojs/pino/blob/HEAD/docs/transports.md
|
||||
// TODO create .cache if missing
|
||||
|
||||
export async function daily(actual: Actual, bank: Bank): Promise<void> {
|
||||
// Fetch transactions from the bank
|
||||
const transactions = await bank.transactionsPastDay(
|
||||
"my_account",
|
||||
"my_access_token",
|
||||
)
|
||||
const transactions = await fetchTransactionsFromPastDay(bank)
|
||||
logger().info(`Fetched ${transactions.length} transactions`)
|
||||
|
||||
// TODO account? id or name?
|
||||
// TODO multiple accounts
|
||||
const accountId = ACTUAL_ACCOUNT_IDS[0] as UUID
|
||||
const actualTransactions = transactions.map((transaction) =>
|
||||
transactionIntoActualTransaction(transaction, ""),
|
||||
bankTransactionIntoActualTransaction(transaction, accountId),
|
||||
)
|
||||
|
||||
// TODO Import transactions into Actual
|
||||
// If multiple accounts, loop over them
|
||||
// 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> {
|
||||
logger().info("Starting application")
|
||||
const actual = await ActualImpl.init()
|
||||
cronJobDaily(await daily(actual, new Sparebank1Impl()))
|
||||
logger().info("Initialized Actual Budget API")
|
||||
|
||||
cronJobDaily(async () => {
|
||||
logger().info("Running daily job")
|
||||
await daily(actual, new Sparebank1Impl())
|
||||
logger().info("Finished daily job")
|
||||
})
|
||||
|
||||
// logger().info("Shutting down")
|
||||
// await actual.shutdown()
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,19 @@
|
||||
import type { Transaction } from "@/sparebank1.ts"
|
||||
import type { TransactionEntity } from "@actual-app/api/@types/loot-core/types/models"
|
||||
import type { UUID } from "node:crypto"
|
||||
|
||||
// TODO more fields / correct fields?
|
||||
export function transactionIntoActualTransaction(
|
||||
export function bankTransactionIntoActualTransaction(
|
||||
transaction: Transaction,
|
||||
account: string,
|
||||
accountId: UUID,
|
||||
): TransactionEntity {
|
||||
return {
|
||||
id: transaction.id,
|
||||
account,
|
||||
amount: transaction.amount,
|
||||
// Transactions with the same id will be ignored
|
||||
imported_id: transaction.id,
|
||||
account: accountId,
|
||||
// The value without decimals
|
||||
amount: transaction.amount * 100,
|
||||
date: transaction.date,
|
||||
payee: transaction.description,
|
||||
}
|
||||
|
@ -1,9 +1,3 @@
|
||||
import {
|
||||
SPAREBANK1_OAUTH_CLIENT_ID,
|
||||
SPAREBANK1_OAUTH_REDIRECT_URI,
|
||||
SPAREBANK1_OAUTH_STATE,
|
||||
} from "../config.ts"
|
||||
|
||||
// TODO move types
|
||||
export interface OAuthTokenResponse {
|
||||
access_token: string
|
||||
@ -40,18 +34,14 @@ export interface Sparebank1 {
|
||||
export class Sparebank1Impl implements Sparebank1 {
|
||||
private baseUrl = "https://api.sparebank1.no"
|
||||
|
||||
// TODO remove?
|
||||
async accessToken(): Promise<OAuthTokenResponse> {
|
||||
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`)
|
||||
throw new Error("Not implemented")
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json()
|
||||
}
|
||||
throw new Error(`Failed to get access token. ${response.statusText}`)
|
||||
// if (response.ok) {
|
||||
// return await response.json()
|
||||
// }
|
||||
// throw new Error(`Failed to get access token. ${response.statusText}`)
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<OAuthTokenResponse> {
|
||||
|
12
tests/main.test.ts
Normal file
12
tests/main.test.ts
Normal file
@ -0,0 +1,12 @@
|
||||
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)
|
||||
})
|
||||
})
|
49
tests/stubs/bankStub.ts
Normal file
49
tests/stubs/bankStub.ts
Normal file
@ -0,0 +1,49 @@
|
||||
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