import { ActualImpl } from "@/actual.ts" import { cronJobDaily } from "@/cron.ts" import { BANK_ACCOUNT_IDS, TRANSACTION_RELATIVE_FROM_DATE, TRANSACTION_RELATIVE_TO_DATE, } from "@/config.ts" import logger from "@common/logger.ts" import type { UUID } from "node:crypto" import { CronJob } from "cron" import dayjs from "dayjs" import type { Actual, Bank, Interval } from "@common/types.ts" import { Sparebank1Impl } from "@sb1impl/sparebank1.ts" // TODO move tsx to devDependency. Requires ts support for Node with support for @ alias // TODO verbatimSyntax in tsconfig, conflicts with jest // 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 async function main(): Promise { logger.info("Starting application") const bank = new Sparebank1Impl() if (process.env.ONCE) { return await runOnce(bank) } await ActualImpl.testConnection() await runCronJob(bank) } export async function moveTransactions( actual: Actual, bank: Bank, ): Promise { 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 runOnce(bank: Bank) { const actual = await ActualImpl.init() registerInterrupt(bank) try { return await moveTransactions(actual, bank) } finally { await actual.shutdown() await shutdown(bank) } } async function runCronJob(bank: Bank): Promise { logger.info("Waiting for CronJob to start") const cronJob = cronJobDaily(async () => { let actual: Actual | undefined try { actual = await ActualImpl.init() await moveTransactions(actual, bank) } catch (exception) { logger.error(exception, "Caught exception at CronJob, shutting down!") await shutdown(bank, cronJob) } finally { await actual?.shutdown() } }) registerInterrupt(bank, cronJob) } let isShuttingDown = false function registerInterrupt( bank: Bank, cronJob: CronJob | undefined = undefined, ): void { process.on("SIGINT", async () => { if (isShuttingDown) return isShuttingDown = true logger.info("Caught interrupt signal") await shutdown(bank, cronJob) }) } async function shutdown( bank: Bank, cronJob: CronJob | undefined = undefined, ): Promise { logger.info("Shutting down, Bye!") await bank.shutdown() cronJob?.stop() } void main()