2024-11-17 22:27:29 +01:00
|
|
|
import { type Actual, ActualImpl } from "@/actual.ts"
|
|
|
|
import { cronJobDaily } from "@/cron.ts"
|
2024-12-22 16:06:20 +01:00
|
|
|
import {
|
|
|
|
type Bank,
|
|
|
|
Sparebank1Impl,
|
|
|
|
type Transaction,
|
|
|
|
} from "@/bank/sparebank1.ts"
|
2024-12-01 20:48:42 +01:00
|
|
|
import { bankTransactionIntoActualTransaction } from "@/mappings.ts"
|
2025-01-22 21:00:04 +01:00
|
|
|
import {
|
|
|
|
ACTUAL_ACCOUNT_IDS,
|
|
|
|
ACTUAL_DATA_DIR,
|
|
|
|
BANK_ACCOUNT_IDS,
|
2025-01-26 19:29:43 +01:00
|
|
|
DB_DIRECTORY,
|
2025-01-25 21:12:49 +01:00
|
|
|
DB_FILENAME,
|
2025-01-22 21:00:04 +01:00
|
|
|
} from "../config.ts"
|
2024-12-25 21:06:42 +01:00
|
|
|
import logger from "@/logger.ts"
|
2024-12-01 19:35:45 +01:00
|
|
|
import type { UUID } from "node:crypto"
|
2025-01-22 21:00:04 +01:00
|
|
|
import { createDb } from "@/bank/db/queries.ts"
|
|
|
|
import * as fs from "node:fs"
|
2025-01-25 22:30:52 +01:00
|
|
|
import { CronJob } from "cron"
|
2024-11-15 22:55:53 +01:00
|
|
|
|
2024-12-01 19:35:45 +01:00
|
|
|
// TODO Transports api for pino https://github.com/pinojs/pino/blob/HEAD/docs/transports.md
|
2025-01-26 17:41:44 +01:00
|
|
|
// TODO move tsx to devDependency. Requires ts support for Node with support for @ alias
|
|
|
|
// TODO global exception handler, log and graceful shutdown
|
2025-01-26 19:29:43 +01:00
|
|
|
// TODO verbatimSyntax in tsconfig, conflicts with jest
|
2025-01-31 16:58:02 +01:00
|
|
|
// 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
|
2024-11-15 22:55:53 +01:00
|
|
|
|
2024-12-01 20:48:42 +01:00
|
|
|
export async function daily(actual: Actual, bank: Bank): Promise<void> {
|
2024-12-01 19:35:45 +01:00
|
|
|
// Fetch transactions from the bank
|
|
|
|
const transactions = await fetchTransactionsFromPastDay(bank)
|
2024-12-23 16:47:07 +01:00
|
|
|
logger.info(`Fetched ${transactions.length} transactions`)
|
2024-11-15 22:55:53 +01:00
|
|
|
|
2024-12-01 19:35:45 +01:00
|
|
|
// TODO multiple accounts
|
|
|
|
const accountId = ACTUAL_ACCOUNT_IDS[0] as UUID
|
|
|
|
const actualTransactions = transactions.map((transaction) =>
|
2025-01-25 22:30:52 +01:00
|
|
|
// TODO move to Bank interface?
|
2024-12-01 20:48:42 +01:00
|
|
|
bankTransactionIntoActualTransaction(transaction, accountId),
|
2024-12-01 19:35:45 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
// TODO Import transactions into Actual
|
|
|
|
// If multiple accounts, loop over them
|
|
|
|
// Get account ID from mapper
|
2024-12-01 20:48:42 +01:00
|
|
|
|
2024-12-25 21:06:42 +01:00
|
|
|
const response = await actual.importTransactions(
|
|
|
|
accountId,
|
|
|
|
actualTransactions,
|
|
|
|
)
|
2024-12-23 16:47:07 +01:00
|
|
|
logger.info(`ImportTransactionsResponse=${JSON.stringify(response)}`)
|
2024-12-01 19:35:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function fetchTransactionsFromPastDay(
|
|
|
|
bank: Bank,
|
|
|
|
): Promise<ReadonlyArray<Transaction>> {
|
2025-01-25 22:30:52 +01:00
|
|
|
const response = await bank.transactionsPastDay(...BANK_ACCOUNT_IDS)
|
2025-01-25 18:01:47 +01:00
|
|
|
return response.transactions
|
2024-11-17 22:27:29 +01:00
|
|
|
}
|
2024-11-15 22:55:53 +01:00
|
|
|
|
2025-01-26 19:29:43 +01:00
|
|
|
function createDirIfMissing(directory: string): void {
|
|
|
|
if (!fs.existsSync(directory)) {
|
|
|
|
logger.info(`Missing '${directory}', creating...`)
|
|
|
|
fs.mkdirSync(directory, { recursive: true })
|
2025-01-22 21:00:04 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-17 22:27:29 +01:00
|
|
|
async function main(): Promise<void> {
|
2024-12-23 16:47:07 +01:00
|
|
|
logger.info("Starting application")
|
2025-01-22 21:00:04 +01:00
|
|
|
|
2025-01-26 19:29:43 +01:00
|
|
|
createDirIfMissing(ACTUAL_DATA_DIR)
|
|
|
|
createDirIfMissing(DB_DIRECTORY)
|
2025-01-22 21:00:04 +01:00
|
|
|
|
2024-11-17 22:27:29 +01:00
|
|
|
const actual = await ActualImpl.init()
|
2025-01-26 19:29:43 +01:00
|
|
|
const databaseFilePath = `${DB_DIRECTORY}/${DB_FILENAME}.sqlite`
|
|
|
|
const db = createDb(databaseFilePath)
|
|
|
|
logger.info(`Started Sqlite database at '${databaseFilePath}'`)
|
2025-01-25 22:30:52 +01:00
|
|
|
const bank = new Sparebank1Impl(db)
|
2024-12-26 12:49:54 +01:00
|
|
|
|
2025-01-25 21:12:49 +01:00
|
|
|
process.on("SIGINT", async () => {
|
|
|
|
logger.info("Caught interrupt signal")
|
|
|
|
await shutdown()
|
|
|
|
})
|
2024-12-01 20:48:42 +01:00
|
|
|
|
2025-01-25 22:30:52 +01:00
|
|
|
let cronJob: CronJob | undefined
|
2025-01-25 21:12:49 +01:00
|
|
|
if (process.env.ONCE) {
|
2025-01-25 22:30:52 +01:00
|
|
|
await daily(actual, bank)
|
2025-01-25 21:12:49 +01:00
|
|
|
await shutdown()
|
|
|
|
return
|
|
|
|
}
|
2024-12-01 20:48:42 +01:00
|
|
|
|
2025-01-25 21:12:49 +01:00
|
|
|
logger.info("Waiting for CRON job to start")
|
2025-01-27 21:30:56 +01:00
|
|
|
// TODO init and shutdown resources when job runs?
|
2025-01-26 19:29:43 +01:00
|
|
|
cronJob = cronJobDaily(async () => await daily(actual, bank))
|
2025-01-25 21:12:49 +01:00
|
|
|
|
|
|
|
async function shutdown(): Promise<void> {
|
2025-01-26 19:29:43 +01:00
|
|
|
logger.info("Shutting down, Bye!")
|
2025-01-25 21:12:49 +01:00
|
|
|
await actual.shutdown()
|
|
|
|
db.close()
|
2025-01-25 22:30:52 +01:00
|
|
|
cronJob?.stop()
|
2025-01-25 21:12:49 +01:00
|
|
|
}
|
2024-11-15 22:55:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void main()
|