// TODO move types import { BANK_INITIAL_REFRESH_TOKEN, BANK_OAUTH_CLIENT_ID, BANK_OAUTH_CLIENT_SECRET, } from "@/../config.ts" import logger from "@/logger.ts" import dayjs from "dayjs" 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 } interface AccessToken { access_token: string expires_in: number } interface RefreshToken { refresh_token: string expires_in: number } export interface Transaction { id: string date: string amount: number description: string cleanedDescription: string remoteAccountName: string [key: string]: string | number | boolean | unknown } export type Bank = Sparebank1 export interface Sparebank1 { transactionsPastDay: ( accountKeys: ReadonlyArray | string, ) => Promise> } export class Sparebank1Impl implements Sparebank1 { private static baseUrl = "https://api.sparebank1.no" private _accessToken: AccessToken | undefined private _refreshToken: RefreshToken | undefined private set accessToken(accessToken: AccessToken) { this._accessToken = accessToken } private set refreshToken(refreshToken: RefreshToken) { this._refreshToken = refreshToken } private async getAccessToken(): Promise { const accessToken = this._accessToken if (!accessToken) { const response = await this.fetchNewRefreshToken() return response.access_token } return accessToken.access_token } private async getRefreshToken(): Promise { const refreshToken = this._refreshToken // TODO check if valid, use jsonwebtoken npm library? const isValid = true if (!refreshToken) { return BANK_INITIAL_REFRESH_TOKEN } else if (isValid) { return refreshToken.refresh_token } else { const response = await this.fetchNewRefreshToken() return response.refresh_token } } async fetchNewRefreshToken(): Promise { const refreshToken: string = await this.getRefreshToken() const queries = new URLSearchParams({ client_id: BANK_OAUTH_CLIENT_ID, client_secret: BANK_OAUTH_CLIENT_SECRET, refresh_token: refreshToken, grant_type: "refresh_token", }) const response = await fetch(`${Sparebank1Impl.baseUrl}/token?${queries}`) if (!response.ok) { throw new Error("Failed to fetch refresh token") } const oAuthToken: OAuthTokenResponse = await response.json() this.accessToken = { access_token: oAuthToken.access_token, expires_in: oAuthToken.expires_in, } this.refreshToken = { refresh_token: oAuthToken.refresh_token, expires_in: oAuthToken.refresh_token_expires_in, } return oAuthToken } async transactionsPastDay( accountKeys: ReadonlyArray | string, ): Promise> { const today = dayjs() const lastDay = today.subtract(1, "day") const queries = new URLSearchParams({ // TODO allow multiple accountKeys accountKey: typeof accountKeys === "string" ? accountKeys : accountKeys[0], fromDate: lastDay.toString(), toDate: today.toString(), }) const accessToken = await this.getAccessToken() const response = await fetch( `${Sparebank1Impl.baseUrl}/transactions?${queries}`, { headers: { Authorization: `Bearer ${accessToken}`, }, }, ) if (response.ok) { return response.json() } else { logger.warn( `transactionsPastDay returned a ${response.status} with the text ${response.statusText}`, ) return [] } } }