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_ACCOUNT_IDS, ACTUAL_DATA_DIR, BANK_ACCOUNT_IDS, 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 create Dockerfile and docker-compose.yml // TODO Gitea workflow // TODO move tsx to devDependency. Requires ts support for Node with support for @ alias // TODO global exception handler, log and graceful shutdown export async function daily(actual: Actual, bank: Bank): Promise { // Fetch transactions from the bank const transactions = await fetchTransactionsFromPastDay(bank) logger.info(`Fetched ${transactions.length} transactions`) // TODO multiple accounts const accountId = ACTUAL_ACCOUNT_IDS[0] as UUID const actualTransactions = transactions.map((transaction) => // TODO move to Bank interface? bankTransactionIntoActualTransaction(transaction, accountId), ) logger.trace({ aMessage: "Mapped from Bank to Actual", from: JSON.stringify(transactions), to: JSON.stringify(actualTransactions), }) // TODO Import transactions into Actual // If multiple accounts, loop over them // Get account ID from mapper const response = await actual.importTransactions( accountId, actualTransactions, ) logger.info(`ImportTransactionsResponse=${JSON.stringify(response)}`) } async function fetchTransactionsFromPastDay( bank: Bank, ): Promise> { const response = await bank.transactionsPastDay(...BANK_ACCOUNT_IDS) return response.transactions } function createCacheDirIfMissing(): void { if (!fs.existsSync(ACTUAL_DATA_DIR)) { logger.info(`Missing '${ACTUAL_DATA_DIR}', creating...`) fs.mkdirSync(ACTUAL_DATA_DIR) } } async function main(): Promise { logger.info("Starting application") createCacheDirIfMissing() const actual = await ActualImpl.init() const databaseFileName = `${DB_FILENAME}.sqlite` const db = createDb(databaseFileName) logger.info(`Started SQLlite database with filename="${databaseFileName}"`) const bank = new Sparebank1Impl(db) process.on("SIGINT", async () => { logger.info("Caught interrupt signal") await shutdown() }) let cronJob: CronJob | undefined if (process.env.ONCE) { await daily(actual, bank) await shutdown() return } logger.info("Waiting for CRON job to start") cronJob = cronJobDaily(async () => { logger.info("Running daily job") await daily(actual, bank) logger.info("Finished daily job") }) async function shutdown(): Promise { logger.info("Shutting down") await actual.shutdown() db.close() cronJob?.stop() } } void main()