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_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
|
||||||
# Sparebank1
|
# Bank
|
||||||
SPAREBANK1_OAUTH_CLIENT_ID=your-client-id
|
BANK_OAUTH_CLIENT_ID=your-client-id
|
||||||
SPAREBANK1_OAUTH_CLIENT_SECRET=your-client-secret
|
BANK_OAUTH_CLIENT_SECRET=your-client-secret
|
||||||
SPAREBANK1_OAUTH_STATE=your-state
|
BANK_OAUTH_STATE=your-state
|
||||||
SPAREBANK1_OAUTH_REDIRECT_URI=your-redirect-uri
|
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
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -12,7 +12,7 @@ lerna-debug.log*
|
|||||||
|
|
||||||
# Caches
|
# Caches
|
||||||
|
|
||||||
data/cache/*
|
.cache
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# 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_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_DATA_DIR = "data/cache"
|
|
||||||
export const ACTUAL_ACCOUNT_IDS = getArrayOrThrow("ACTUAL_ACCOUNT_IDS")
|
export const ACTUAL_ACCOUNT_IDS = getArrayOrThrow("ACTUAL_ACCOUNT_IDS")
|
||||||
|
export const ACTUAL_DATA_DIR = ".cache"
|
||||||
|
|
||||||
export const SPAREBANK1_OAUTH_CLIENT_ID = getOrThrow(
|
export const BANK_OAUTH_CLIENT_ID = getOrThrow("BANK_OAUTH_CLIENT_ID")
|
||||||
"SPAREBANK1_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 SPAREBANK1_OAUTH_CLIENT_SECRET = getOrThrow(
|
export const BANK_OAUTH_STATE = getOrThrow("BANK_OAUTH_STATE")
|
||||||
"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_ACCOUNT_IDS = getArrayOrThrow("BANK_ACCOUNT_IDS")
|
export const BANK_ACCOUNT_IDS = getArrayOrThrow("BANK_ACCOUNT_IDS")
|
||||||
|
|
||||||
function getOrThrow(key: string): string {
|
function getOrThrow(key: string): string {
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --import=tsx ./src/main.ts | pino-pretty",
|
"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}\""
|
"format": "prettier --write \"./**/*.{js,mjs,ts,md,json}\""
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
@ -3,7 +3,6 @@ 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"
|
||||||
@ -53,28 +52,3 @@ 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()
|
|
||||||
}
|
|
||||||
|
14
src/main.ts
14
src/main.ts
@ -1,14 +1,15 @@
|
|||||||
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, 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 { ACTUAL_ACCOUNT_IDS, BANK_ACCOUNT_IDS } from "../config.ts"
|
||||||
import logger from "pino"
|
import logger from "pino"
|
||||||
import type { UUID } from "node:crypto"
|
import type { UUID } from "node:crypto"
|
||||||
|
|
||||||
// TODO Transports api for pino https://github.com/pinojs/pino/blob/HEAD/docs/transports.md
|
// 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
|
// Fetch transactions from the bank
|
||||||
const transactions = await fetchTransactionsFromPastDay(bank)
|
const transactions = await fetchTransactionsFromPastDay(bank)
|
||||||
logger().info(`Fetched ${transactions.length} transactions`)
|
logger().info(`Fetched ${transactions.length} transactions`)
|
||||||
@ -16,12 +17,14 @@ async function daily(actual: Actual, bank: Bank): Promise<void> {
|
|||||||
// TODO multiple accounts
|
// TODO multiple accounts
|
||||||
const accountId = ACTUAL_ACCOUNT_IDS[0] as UUID
|
const accountId = ACTUAL_ACCOUNT_IDS[0] as UUID
|
||||||
const actualTransactions = transactions.map((transaction) =>
|
const actualTransactions = transactions.map((transaction) =>
|
||||||
transactionIntoActualTransaction(transaction, accountId),
|
bankTransactionIntoActualTransaction(transaction, accountId),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
|
// TODO TypeError: Cannot read properties of undefined (reading 'timestamp')
|
||||||
await actual.importTransactions(accountId, actualTransactions)
|
await actual.importTransactions(accountId, actualTransactions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,12 +39,15 @@ async function fetchTransactionsFromPastDay(
|
|||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
logger().info("Starting application")
|
logger().info("Starting application")
|
||||||
const actual = await ActualImpl.init()
|
const actual = await ActualImpl.init()
|
||||||
|
logger().info("Initialized Actual Budget API")
|
||||||
|
|
||||||
cronJobDaily(async () => {
|
cronJobDaily(async () => {
|
||||||
logger().info("Running daily job")
|
logger().info("Running daily job")
|
||||||
await daily(actual, new Sparebank1Impl())
|
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()
|
// 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"
|
import type { UUID } from "node:crypto"
|
||||||
|
|
||||||
// TODO more fields / correct fields?
|
// TODO more fields / correct fields?
|
||||||
export function transactionIntoActualTransaction(
|
export function bankTransactionIntoActualTransaction(
|
||||||
transaction: Transaction,
|
transaction: Transaction,
|
||||||
accountId: UUID,
|
accountId: UUID,
|
||||||
): TransactionEntity {
|
): TransactionEntity {
|
||||||
return {
|
return {
|
||||||
id: transaction.id,
|
id: transaction.id,
|
||||||
|
// Transactions with the same id will be ignored
|
||||||
|
imported_id: transaction.id,
|
||||||
account: accountId,
|
account: accountId,
|
||||||
amount: transaction.amount,
|
// The value without decimals
|
||||||
|
amount: transaction.amount * 100,
|
||||||
date: transaction.date,
|
date: transaction.date,
|
||||||
payee: transaction.description,
|
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