2025-02-16 18:22:10 +01:00
|
|
|
import * as Transactions from "@sb1/transactions.ts"
|
|
|
|
import * as Oauth from "@sb1/oauth.ts"
|
2025-02-09 12:35:08 +01:00
|
|
|
import {
|
|
|
|
BANK_INITIAL_REFRESH_TOKEN,
|
|
|
|
BANK_OAUTH_CLIENT_ID,
|
|
|
|
BANK_OAUTH_CLIENT_SECRET,
|
2025-02-13 21:07:30 +01:00
|
|
|
DB_DIRECTORY,
|
|
|
|
DB_FILENAME,
|
2025-02-09 13:01:34 +01:00
|
|
|
} from "@/config.ts"
|
2025-01-25 18:01:47 +01:00
|
|
|
import {
|
2025-02-06 19:13:23 +01:00
|
|
|
clearTokens,
|
2025-02-13 21:07:30 +01:00
|
|
|
createDb,
|
2025-01-25 18:01:47 +01:00
|
|
|
fetchToken,
|
|
|
|
insertTokens,
|
2025-02-13 21:07:30 +01:00
|
|
|
} from "@sb1impl/db/queries.ts"
|
|
|
|
import { bankTransactionIntoActualTransaction } from "./mappings.ts"
|
2025-02-16 18:22:10 +01:00
|
|
|
import { createDirIfMissing } from "@/fs.ts"
|
|
|
|
import logger from "@common/logger.ts"
|
|
|
|
import dayjs from "dayjs"
|
|
|
|
|
2025-02-13 21:07:30 +01:00
|
|
|
import type { ActualTransaction, Bank, Interval } from "@common/types.ts"
|
|
|
|
import type { TokenResponse } from "@sb1impl/db/types.ts"
|
2025-02-16 18:22:10 +01:00
|
|
|
import type { OAuthTokenResponse } from "@sb1/types.ts"
|
|
|
|
import type { Database } from "better-sqlite3"
|
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
|
|
|
|
|
2025-02-13 21:07:30 +01:00
|
|
|
constructor() {
|
|
|
|
createDirIfMissing(DB_DIRECTORY)
|
|
|
|
const databaseFilePath = `${DB_DIRECTORY}/${DB_FILENAME}.sqlite`
|
|
|
|
this.db = createDb(databaseFilePath)
|
2024-12-26 14:08:09 +01:00
|
|
|
}
|
2024-11-17 22:27:29 +01:00
|
|
|
|
2025-02-16 18:22:10 +01:00
|
|
|
// TODO if not cleared rerun later
|
2025-02-06 18:56:51 +01:00
|
|
|
async fetchTransactions(
|
|
|
|
interval: Interval,
|
|
|
|
...accountKeys: ReadonlyArray<string>
|
|
|
|
): Promise<ReadonlyArray<ActualTransaction>> {
|
2025-02-09 12:35:08 +01:00
|
|
|
const response = await Transactions.list(
|
2025-02-06 18:56:51 +01:00
|
|
|
await this.getAccessToken(),
|
|
|
|
accountKeys,
|
|
|
|
interval,
|
|
|
|
)
|
2025-02-16 18:22:10 +01:00
|
|
|
|
|
|
|
return response.transactions
|
|
|
|
.map((transaction) => {
|
|
|
|
const actualTransaction =
|
|
|
|
bankTransactionIntoActualTransaction(transaction)
|
|
|
|
|
|
|
|
return actualTransaction.cleared === true ? actualTransaction : null
|
|
|
|
})
|
|
|
|
.filter((transaction) => transaction !== null)
|
2025-02-06 18:56:51 +01:00
|
|
|
}
|
|
|
|
|
2025-02-13 21:07:30 +01:00
|
|
|
shutdown(): void {
|
|
|
|
this.db.close()
|
|
|
|
}
|
|
|
|
|
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-02-06 19:13:23 +01:00
|
|
|
logger.warn("Refresh token expired, deleting tokens from database")
|
|
|
|
clearTokens(this.db)
|
2025-01-22 21:00:04 +01:00
|
|
|
throw new Error("Refresh token is expired. Create a new one")
|
2024-12-25 21:06:42 +01:00
|
|
|
}
|
|
|
|
|
2025-02-06 18:56:51 +01:00
|
|
|
private async fetchNewTokens(): Promise<OAuthTokenResponse> {
|
2025-01-22 21:00:04 +01:00
|
|
|
const refreshToken = await this.getRefreshToken()
|
2025-02-09 12:35:08 +01:00
|
|
|
const result = await Oauth.refreshToken(
|
|
|
|
BANK_OAUTH_CLIENT_ID,
|
|
|
|
BANK_OAUTH_CLIENT_SECRET,
|
|
|
|
refreshToken,
|
|
|
|
)
|
2024-12-26 14:08:09 +01:00
|
|
|
|
|
|
|
if (result.status === "failure") {
|
2025-02-06 19:37:11 +01:00
|
|
|
throw 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
|
|
|
}
|
2024-11-15 22:55:53 +01:00
|
|
|
}
|