Changed:
- Env key to be more generic - Cache directory to .cache - Multiply bank amount by 100 to get correct conversion rate Removed: - Unused functions in Actual.ts Added: - Test for main functionality - BankStub for testing - Added imported_id to Actual Transaction to avoid duplicates
This commit is contained in:
parent
cc325b9f08
commit
9ed0a19393
10
.env.example
10
.env.example
@ -2,9 +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)
|
||||
|
||||
|
16
config.ts
16
config.ts
@ -7,19 +7,13 @@ 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 {
|
||||
|
@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node --import=tsx ./src/main.ts | pino-pretty",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"test": "node --test --experimental-strip-types ./tests/**",
|
||||
"format": "prettier --write \"./**/*.{js,mjs,ts,md,json}\""
|
||||
},
|
||||
"keywords": [],
|
||||
|
@ -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()
|
||||
}
|
||||
|
14
src/main.ts
14
src/main.ts
@ -1,14 +1,15 @@
|
||||
import { type Actual, ActualImpl } from "@/actual.ts"
|
||||
import { cronJobDaily } from "@/cron.ts"
|
||||
import { type Bank, Sparebank1Impl, type Transaction } from "@/sparebank1.ts"
|
||||
import { transactionIntoActualTransaction } from "@/mappings.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"
|
||||
|
||||
// TODO Transports api for pino https://github.com/pinojs/pino/blob/HEAD/docs/transports.md
|
||||
// TODO create .cache if missing
|
||||
|
||||
async function daily(actual: Actual, bank: Bank): Promise<void> {
|
||||
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`)
|
||||
@ -16,12 +17,14 @@ async function daily(actual: Actual, bank: Bank): Promise<void> {
|
||||
// TODO multiple accounts
|
||||
const accountId = ACTUAL_ACCOUNT_IDS[0] as UUID
|
||||
const actualTransactions = transactions.map((transaction) =>
|
||||
transactionIntoActualTransaction(transaction, accountId),
|
||||
bankTransactionIntoActualTransaction(transaction, accountId),
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@ -36,12 +39,15 @@ async function fetchTransactionsFromPastDay(
|
||||
async function main(): Promise<void> {
|
||||
logger().info("Starting application")
|
||||
const actual = await ActualImpl.init()
|
||||
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")
|
||||
|
||||
// logger().info("Shutting down")
|
||||
// await actual.shutdown()
|
||||
}
|
||||
|
||||
|
@ -3,14 +3,17 @@ import type { TransactionEntity } from "@actual-app/api/@types/loot-core/types/m
|
||||
import type { UUID } from "node:crypto"
|
||||
|
||||
// TODO more fields / correct fields?
|
||||
export function transactionIntoActualTransaction(
|
||||
export function bankTransactionIntoActualTransaction(
|
||||
transaction: Transaction,
|
||||
accountId: UUID,
|
||||
): TransactionEntity {
|
||||
return {
|
||||
id: transaction.id,
|
||||
// Transactions with the same id will be ignored
|
||||
imported_id: transaction.id,
|
||||
account: accountId,
|
||||
amount: transaction.amount,
|
||||
// The value without decimals
|
||||
amount: transaction.amount * 100,
|
||||
date: transaction.date,
|
||||
payee: transaction.description,
|
||||
}
|
||||
|
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