import { BANK_INITIAL_REFRESH_TOKEN } from "@/../config.ts" import logger from "@/logger.ts" import dayjs, { Dayjs } from "dayjs" import { Database } from "better-sqlite3" import { clearTokens, fetchToken, insertTokens, type TokenResponse, } from "@/bank/db/queries.ts" import * as Api from "./sparebank1Api.ts" import { ActualTransaction } from "@/actual.ts" import { bankTransactionIntoActualTransaction } from "@/mappings.ts" export interface OAuthTokenResponse { access_token: string expires_in: number refresh_token_expires_in: number refresh_token_absolute_expires_in: number token_type: "Bearer" refresh_token: string } export type BookingStatus = "PENDING" | "BOOKED" export interface Transaction { id: string nonUniqueId: string // The Id of the account accountKey: string // Unix time date: number // Amount in NOK amount: number cleanedDescription: string remoteAccountName: string bookingStatus: BookingStatus [key: string]: string | number | boolean | unknown } export interface TransactionResponse { transactions: ReadonlyArray } export interface Bank { fetchTransactions: ( interval: Interval, ...accountKeys: ReadonlyArray ) => Promise> } export interface Interval { fromDate: Dayjs toDate: Dayjs } export class Sparebank1Impl implements Bank { private readonly db: Database constructor(db: Database) { this.db = db } async fetchTransactions( interval: Interval, ...accountKeys: ReadonlyArray ): Promise> { const response = await Api.transactions( await this.getAccessToken(), accountKeys, interval, ) const sparebankTransactions = response.transactions return sparebankTransactions.map(bankTransactionIntoActualTransaction) } 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 Api.refreshToken(refreshToken) if (result.status === "failure") { throw logger.error({ err: new Error(`Failed to fetch refresh token: '${result.data}'`), }) } const oAuthToken = result.data insertTokens(this.db, oAuthToken) return oAuthToken } }