142 lines
3.6 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 { insertTokens } from "@/bank/db/queries.ts"
import * as Api from "./sparebank1Api.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
}
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> | string,
) => Promise<ReadonlyArray<Transaction>>
}
export class Sparebank1Impl implements Sparebank1 {
private static baseUrl = "https://api.sparebank1.no"
private _accessToken: AccessToken | undefined
private _refreshToken: RefreshToken | undefined
private readonly db: Database
constructor(db: Database) {
this.db = db
}
private set accessToken(accessToken: AccessToken) {
this._accessToken = accessToken
}
private set refreshToken(refreshToken: RefreshToken) {
this._refreshToken = refreshToken
}
private async getAccessToken(): Promise<string> {
const accessToken = this._accessToken
if (!accessToken) {
const response = await this.fetchNewRefreshToken()
return response.access_token
}
return accessToken.access_token
}
private async getRefreshToken(): Promise<string> {
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<OAuthTokenResponse> {
const refreshToken: string = await this.getRefreshToken()
const result = await Api.refreshToken(refreshToken)
if (result.status === "failure") {
throw new Error("Failed to fetch refresh token")
}
const oAuthToken = result.data
insertTokens(this.db, oAuthToken)
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> | string,
): Promise<ReadonlyArray<Transaction>> {
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 []
}
}
2024-11-15 22:55:53 +01:00
}