import * as Transactions from "@sb1/transactions.ts" import * as Oauth from "@sb1/oauth.ts" import { BANK_INITIAL_REFRESH_TOKEN, BANK_OAUTH_CLIENT_ID, BANK_OAUTH_CLIENT_SECRET, DB_DIRECTORY, DB_FILENAME, } from "@/config.ts" import { clearTokens, createDb, fetchToken, insertTokens, } from "@sb1impl/db/queries.ts" import { bankTransactionIntoActualTransaction } from "./mappings.ts" import { createDirIfMissing } from "@/fs.ts" import logger from "@common/logger.ts" import dayjs from "dayjs" import type { ActualTransaction, Bank, Interval } from "@common/types.ts" import type { TokenResponse } from "@sb1impl/db/types.ts" import type { OAuthTokenResponse } from "@sb1/types.ts" import type { Database } from "better-sqlite3" export class Sparebank1Impl implements Bank { private readonly db: Database constructor() { createDirIfMissing(DB_DIRECTORY) const databaseFilePath = `${DB_DIRECTORY}/${DB_FILENAME}.sqlite` this.db = createDb(databaseFilePath) } // TODO if not cleared rerun later async fetchTransactions( interval: Interval, ...accountKeys: ReadonlyArray ): Promise> { const response = await Transactions.list( await this.getAccessToken(), accountKeys, interval, ) return response.transactions .map((transaction) => { const actualTransaction = bankTransactionIntoActualTransaction(transaction) return actualTransaction.cleared === true ? actualTransaction : null }) .filter((transaction) => transaction !== null) } shutdown(): void { this.db.close() } private async getAccessToken(): Promise { const accessToken = fetchToken(this.db, "access-token") if (accessToken && this.isValidToken(accessToken)) { return accessToken.token } const response = await this.fetchNewTokens() return response.access_token } private isValidToken(tokenResponse: TokenResponse): boolean { // TODO make sure the same timezone is used. Db uses UTC return dayjs().isBefore(tokenResponse.expires_at) } private async getRefreshToken(): Promise { const tokenResponse = fetchToken(this.db, "refresh-token") if (!tokenResponse) { return BANK_INITIAL_REFRESH_TOKEN } else if (this.isValidToken(tokenResponse)) { return tokenResponse.token } logger.warn("Refresh token expired, deleting tokens from database") clearTokens(this.db) throw new Error("Refresh token is expired. Create a new one") } private async fetchNewTokens(): Promise { const refreshToken = await this.getRefreshToken() const result = await Oauth.refreshToken( BANK_OAUTH_CLIENT_ID, BANK_OAUTH_CLIENT_SECRET, refreshToken, ) if (result.status === "failure") { throw new Error(`Failed to fetch refresh token: '${result.data}'`) } const oAuthToken = result.data insertTokens(this.db, oAuthToken) return oAuthToken } }