109 lines
3.0 KiB
TypeScript
Raw Normal View History

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"
2024-11-15 22:55:53 +01:00
// 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
2024-11-15 22:55:53 +01:00
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`)
2024-11-15 22:55:53 +01:00
// 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<ReadonlyArray<Transaction>> {
const response = await bank.transactionsPastDay(...BANK_ACCOUNT_IDS)
return response.transactions
}
2024-11-15 22:55:53 +01:00
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<void> {
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<void> {
logger.info("Shutting down")
await actual.shutdown()
db.close()
cronJob?.stop()
}
2024-11-15 22:55:53 +01:00
}
void main()