import { type Actual, ActualImpl } from "@/actual.ts" import { cronJobDaily } from "@/cron.ts" import { type Bank, type Interval, Sparebank1Impl } from "@/bank/sparebank1.ts" import { ACTUAL_DATA_DIR, BANK_ACCOUNT_IDS, DB_DIRECTORY, DB_FILENAME, TRANSACTION_RELATIVE_FROM_DATE, TRANSACTION_RELATIVE_TO_DATE, } from "../config.ts" import logger from "@common/logger.ts" import type { UUID } from "node:crypto" import { createDb } from "@/bank/db/queries.ts" import { CronJob } from "cron" import { createDirsIfMissing } from "@/fs.ts" import dayjs from "dayjs" // 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 // TODO do not fetch if saturday or sunday export async function daily(actual: Actual, bank: Bank): Promise { // Fetch transactions from the bank const actualTransactions = await bank.fetchTransactions( relativeInterval(), ...BANK_ACCOUNT_IDS, ) logger.info(`Fetched ${actualTransactions.length} transactions`) const transactionsByAccount = Object.groupBy( actualTransactions, (transaction) => transaction.account, ) const responses = await Promise.all( Object.entries(transactionsByAccount).map(([accountId, transactions]) => actual.importTransactions(accountId as UUID, transactions || []), ), ) logger.debug( responses.map((response) => ({ added: response.added, updated: response.updated, })), "Finished importing transactions", ) } function relativeInterval(): Interval { const today = dayjs() return { fromDate: today.subtract(TRANSACTION_RELATIVE_FROM_DATE, "days"), toDate: today.subtract(TRANSACTION_RELATIVE_TO_DATE, "days"), } } async function main(): Promise { logger.info("Starting application") createDirsIfMissing(ACTUAL_DATA_DIR, 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 { logger.info("Shutting down, Bye!") await actual.shutdown() db.close() cronJob?.stop() } } void main()