130 lines
3.7 KiB
TypeScript
Raw Normal View History

// TODO move types
import { BANK_INITIAL_REFRESH_TOKEN } from "@/../config.ts"
import logger from "@/logger.ts"
import dayjs from "dayjs"
import { Database } from "better-sqlite3"
import {
fetchToken,
insertTokens,
type TokenResponse,
} from "@/bank/db/queries.ts"
import * as Api from "./sparebank1Api.ts"
import { toISODateString } from "@/date.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 interface Transaction {
id: string
nonUniqueId: string
date: number // Unix time
amount: number // Amount in NOK
cleanedDescription: string
remoteAccountName: string
[key: string]: string | number | boolean | unknown
}
export interface TransactionResponse {
transactions: ReadonlyArray<Transaction>
}
export type Bank = Sparebank1
export interface Sparebank1 {
transactionsPastDay: (
accountKeys: ReadonlyArray<string> | string,
) => Promise<TransactionResponse>
}
export class Sparebank1Impl implements Sparebank1 {
private static baseUrl = "https://api.sparebank1.no"
private readonly db: Database
constructor(db: Database) {
this.db = db
}
private async getAccessToken(): Promise<string> {
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<string> {
const tokenResponse = fetchToken(this.db, "refresh-token")
logger.debug(`Database returned refresh token: '%o'`, tokenResponse)
if (!tokenResponse) {
return BANK_INITIAL_REFRESH_TOKEN
} else if (this.isValidToken(tokenResponse)) {
return tokenResponse.token
}
// TODO clear database, if refresh token is invalid, will cause Exceptions on each call
throw new Error("Refresh token is expired. Create a new one")
}
async fetchNewTokens(): Promise<OAuthTokenResponse> {
const refreshToken = await this.getRefreshToken()
logger.debug(`Found refresh token '${refreshToken}'`)
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
}
async transactionsPastDay(
accountKeys: ReadonlyArray<string> | string,
): Promise<TransactionResponse> {
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: toISODateString(lastDay),
toDate: toISODateString(today),
})
const accessToken = await this.getAccessToken()
logger.debug(`Found access token '${accessToken}'`)
const url = `${Sparebank1Impl.baseUrl}/personal/banking/transactions?${queries}`
logger.debug(`Sending GET request to '${url}'`)
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/vnd.sparebank1.v1+json;charset=utf-8",
},
})
logger.debug(`Received response with status '${response.status}'`)
if (response.ok) {
return response.json()
} else {
logger.warn(await response.json())
return { transactions: [] }
}
}
2024-11-15 22:55:53 +01:00
}