2025-01-31 16:58:02 +01:00
|
|
|
import {
|
|
|
|
BANK_INITIAL_REFRESH_TOKEN,
|
|
|
|
TRANSACTION_RELATIVE_FROM_DATE,
|
|
|
|
TRANSACTION_RELATIVE_TO_DATE,
|
|
|
|
} 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"
|
2025-01-25 18:01:47 +01:00
|
|
|
import {
|
|
|
|
fetchToken,
|
|
|
|
insertTokens,
|
|
|
|
type TokenResponse,
|
|
|
|
} from "@/bank/db/queries.ts"
|
2024-12-26 14:08:09 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2025-01-27 21:30:56 +01:00
|
|
|
export type BookingStatus = "PENDING" | "BOOKED"
|
|
|
|
|
2024-11-17 22:27:29 +01:00
|
|
|
export interface Transaction {
|
|
|
|
id: string
|
2025-01-25 18:01:47 +01:00
|
|
|
nonUniqueId: string
|
2025-02-02 12:37:43 +01:00
|
|
|
// The Id of the account
|
|
|
|
accountKey: string
|
|
|
|
// Unix time
|
|
|
|
date: number
|
|
|
|
// Amount in NOK
|
|
|
|
amount: number
|
2024-11-17 22:27:29 +01:00
|
|
|
cleanedDescription: string
|
|
|
|
remoteAccountName: string
|
2025-01-27 21:30:56 +01:00
|
|
|
bookingStatus: BookingStatus
|
2024-11-17 22:27:29 +01:00
|
|
|
|
|
|
|
[key: string]: string | number | boolean | unknown
|
|
|
|
}
|
|
|
|
|
2025-01-25 18:01:47 +01:00
|
|
|
export interface TransactionResponse {
|
|
|
|
transactions: ReadonlyArray<Transaction>
|
|
|
|
}
|
|
|
|
|
2025-01-27 21:30:56 +01:00
|
|
|
export interface Bank {
|
2024-11-17 22:27:29 +01:00
|
|
|
transactionsPastDay: (
|
2025-01-25 22:30:52 +01:00
|
|
|
...accountKeys: ReadonlyArray<string>
|
2025-01-25 18:01:47 +01:00
|
|
|
) => Promise<TransactionResponse>
|
2024-11-17 22:27:29 +01:00
|
|
|
}
|
|
|
|
|
2025-01-27 21:30:56 +01:00
|
|
|
export class Sparebank1Impl implements Bank {
|
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
|
|
|
|
2025-01-22 21:00:04 +01:00
|
|
|
private async getAccessToken(): Promise<string> {
|
|
|
|
const accessToken = fetchToken(this.db, "access-token")
|
2024-11-17 22:27:29 +01:00
|
|
|
|
2025-01-22 21:00:04 +01:00
|
|
|
if (accessToken && this.isValidToken(accessToken)) {
|
|
|
|
return accessToken.token
|
|
|
|
}
|
|
|
|
const response = await this.fetchNewTokens()
|
|
|
|
return response.access_token
|
2024-11-17 22:27:29 +01:00
|
|
|
}
|
|
|
|
|
2025-01-22 21:00:04 +01:00
|
|
|
private isValidToken(tokenResponse: TokenResponse): boolean {
|
2025-01-25 18:01:47 +01:00
|
|
|
// TODO make sure the same timezone is used. Db uses UTC
|
|
|
|
return dayjs().isBefore(tokenResponse.expires_at)
|
2024-12-25 21:06:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private async getRefreshToken(): Promise<string> {
|
2025-01-22 21:00:04 +01:00
|
|
|
const tokenResponse = fetchToken(this.db, "refresh-token")
|
2025-01-31 16:58:02 +01:00
|
|
|
|
2025-01-22 21:00:04 +01:00
|
|
|
if (!tokenResponse) {
|
2024-12-25 21:06:42 +01:00
|
|
|
return BANK_INITIAL_REFRESH_TOKEN
|
2025-01-22 21:00:04 +01:00
|
|
|
} else if (this.isValidToken(tokenResponse)) {
|
|
|
|
return tokenResponse.token
|
2024-12-25 21:06:42 +01:00
|
|
|
}
|
2025-01-22 21:00:04 +01:00
|
|
|
// 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")
|
2024-12-25 21:06:42 +01:00
|
|
|
}
|
|
|
|
|
2025-01-22 21:00:04 +01:00
|
|
|
async fetchNewTokens(): Promise<OAuthTokenResponse> {
|
|
|
|
const refreshToken = await this.getRefreshToken()
|
2024-12-26 14:08:09 +01:00
|
|
|
const result = await Api.refreshToken(refreshToken)
|
|
|
|
|
|
|
|
if (result.status === "failure") {
|
2025-01-25 18:01:47 +01:00
|
|
|
throw logger.error({
|
|
|
|
err: new Error(`Failed to fetch refresh token: '${result.data}'`),
|
|
|
|
})
|
2024-12-25 21:06:42 +01:00
|
|
|
}
|
2024-12-26 14:08:09 +01:00
|
|
|
const oAuthToken = result.data
|
|
|
|
|
|
|
|
insertTokens(this.db, oAuthToken)
|
2024-12-25 21:06:42 +01:00
|
|
|
return oAuthToken
|
2024-11-17 22:27:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async transactionsPastDay(
|
2025-01-25 22:30:52 +01:00
|
|
|
...accountKeys: ReadonlyArray<string>
|
2025-01-25 18:01:47 +01:00
|
|
|
): Promise<TransactionResponse> {
|
2024-12-25 21:06:42 +01:00
|
|
|
const today = dayjs()
|
2025-01-31 16:58:02 +01:00
|
|
|
const fromDate = today.subtract(TRANSACTION_RELATIVE_FROM_DATE, "days")
|
|
|
|
const toDate = today.subtract(TRANSACTION_RELATIVE_TO_DATE, "days")
|
2025-01-25 22:30:52 +01:00
|
|
|
return await Api.transactions(await this.getAccessToken(), accountKeys, {
|
2025-01-31 16:58:02 +01:00
|
|
|
fromDate,
|
|
|
|
toDate,
|
2025-01-25 18:01:47 +01:00
|
|
|
})
|
2024-11-17 22:27:29 +01:00
|
|
|
}
|
2024-11-15 22:55:53 +01:00
|
|
|
}
|