Martin Berg Alstad 75ad4946d2
All checks were successful
Deploy application / deploy (push) Successful in 3s
🎉 Allow syncing multiple accounts at once
- By defining multiple account keys in the .env, will fetch from all and upload to correct account
- If transaction id is 0, will not be marked as cleared
- Added accountKey to Transaction interface
2025-02-02 12:37:43 +01:00

109 lines
3.2 KiB
TypeScript

import { type Actual, ActualImpl } from "@/actual.ts"
import { cronJobDaily } from "@/cron.ts"
import {
type Bank,
Sparebank1Impl,
type Transaction,
} from "@/bank/sparebank1.ts"
import { bankTransactionIntoActualTransaction } from "@/mappings.ts"
import {
ACTUAL_DATA_DIR,
BANK_ACCOUNT_IDS,
DB_DIRECTORY,
DB_FILENAME,
} from "../config.ts"
import logger from "@/logger.ts"
import type { UUID } from "node:crypto"
import { createDb } from "@/bank/db/queries.ts"
import * as fs from "node:fs"
import { CronJob } from "cron"
// TODO Transports api for pino https://github.com/pinojs/pino/blob/HEAD/docs/transports.md
// TODO move tsx to devDependency. Requires ts support for Node with support for @ alias
// TODO verbatimSyntax in tsconfig, conflicts with jest
// TODO multi module project. Main | DAL | Sparebank1 impl
// TODO store last fetched date in db, and refetch from that date, if app has been offline for some time
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`)
const actualTransactions = transactions.map((transaction) =>
// TODO move to Bank interface?
bankTransactionIntoActualTransaction(transaction),
)
const transactionsGroup = Object.groupBy(
actualTransactions,
(transaction) => transaction.account,
)
const response = await Promise.all(
Object.entries(transactionsGroup).map(([accountId, transactions]) =>
actual.importTransactions(accountId as UUID, transactions || []),
),
)
logger.debug(response, "Finished importing transactions")
}
async function fetchTransactionsFromPastDay(
bank: Bank,
): Promise<ReadonlyArray<Transaction>> {
const response = await bank.transactionsPastDay(...BANK_ACCOUNT_IDS)
return response.transactions
}
function createDirIfMissing(directory: string): void {
if (!fs.existsSync(directory)) {
logger.info(`Missing '${directory}', creating...`)
fs.mkdirSync(directory, { recursive: true })
}
}
async function main(): Promise<void> {
logger.info("Starting application")
createDirIfMissing(ACTUAL_DATA_DIR)
createDirIfMissing(DB_DIRECTORY)
const actual = await ActualImpl.init()
const databaseFilePath = `${DB_DIRECTORY}/${DB_FILENAME}.sqlite`
const db = createDb(databaseFilePath)
logger.info(`Started Sqlite database at '${databaseFilePath}'`)
const bank = new Sparebank1Impl(db)
process.on("SIGINT", async () => {
logger.info("Caught interrupt signal")
await shutdown()
})
let cronJob: CronJob | undefined
if (process.env.ONCE) {
try {
return await daily(actual, bank)
} finally {
await shutdown()
}
}
logger.info("Waiting for CRON job to start")
// TODO init and shutdown resources when job runs?
try {
cronJob = cronJobDaily(async () => await daily(actual, bank))
} catch (exception) {
logger.error(exception, "Caught exception at daily job, shutting down!")
await shutdown()
}
async function shutdown(): Promise<void> {
logger.info("Shutting down, Bye!")
await actual.shutdown()
db.close()
cronJob?.stop()
}
}
void main()