2024-11-17 22:27:29 +01:00
|
|
|
// TODO move types
|
2024-12-26 14:08:09 +01:00
|
|
|
import { BANK_INITIAL_REFRESH_TOKEN } from "@/../config.ts"
|
2024-12-25 21:06:42 +01:00
|
|
|
import logger from "@/logger.ts"
|
|
|
|
import dayjs from "dayjs"
|
2024-12-26 14:08:09 +01:00
|
|
|
import { Database } from "better-sqlite3"
|
|
|
|
import { insertTokens } from "@/bank/db/queries.ts"
|
|
|
|
import * as Api from "./sparebank1Api.ts"
|
2024-12-25 21:06:42 +01:00
|
|
|
|
2024-11-17 22:27:29 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-12-25 21:06:42 +01:00
|
|
|
interface AccessToken {
|
|
|
|
access_token: string
|
|
|
|
expires_in: number
|
|
|
|
}
|
|
|
|
|
|
|
|
interface RefreshToken {
|
|
|
|
refresh_token: string
|
|
|
|
expires_in: number
|
|
|
|
}
|
|
|
|
|
2024-11-17 22:27:29 +01:00
|
|
|
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 {
|
2024-12-25 21:06:42 +01:00
|
|
|
private static baseUrl = "https://api.sparebank1.no"
|
|
|
|
private _accessToken: AccessToken | undefined
|
|
|
|
private _refreshToken: RefreshToken | undefined
|
2024-12-26 14:08:09 +01:00
|
|
|
private readonly db: Database
|
|
|
|
|
|
|
|
constructor(db: Database) {
|
|
|
|
this.db = db
|
|
|
|
}
|
2024-11-17 22:27:29 +01:00
|
|
|
|
2024-12-25 21:06:42 +01:00
|
|
|
private set accessToken(accessToken: AccessToken) {
|
|
|
|
this._accessToken = accessToken
|
|
|
|
}
|
2024-11-17 22:27:29 +01:00
|
|
|
|
2024-12-25 21:06:42 +01:00
|
|
|
private set refreshToken(refreshToken: RefreshToken) {
|
|
|
|
this._refreshToken = refreshToken
|
2024-11-17 22:27:29 +01:00
|
|
|
}
|
|
|
|
|
2024-12-25 21:06:42 +01:00
|
|
|
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()
|
2024-12-26 14:08:09 +01:00
|
|
|
const result = await Api.refreshToken(refreshToken)
|
|
|
|
|
|
|
|
if (result.status === "failure") {
|
2024-12-25 21:06:42 +01:00
|
|
|
throw new Error("Failed to fetch refresh token")
|
|
|
|
}
|
2024-12-26 14:08:09 +01:00
|
|
|
const oAuthToken = result.data
|
|
|
|
|
|
|
|
insertTokens(this.db, oAuthToken)
|
2024-12-25 21:06:42 +01:00
|
|
|
|
|
|
|
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
|
2024-11-17 22:27:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async transactionsPastDay(
|
|
|
|
accountKeys: ReadonlyArray<string> | string,
|
|
|
|
): Promise<ReadonlyArray<Transaction>> {
|
2024-12-25 21:06:42 +01:00
|
|
|
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-17 22:27:29 +01:00
|
|
|
}
|
2024-11-15 22:55:53 +01:00
|
|
|
}
|