✏ README, fix start-once bug and refactor
- Added some configuration and running to README - Refactored some code - Fixed exception when stopping a start-once script - Only allow running with pnpm - Moved transactions into sparebank1Api.ts - Formatted
This commit is contained in:
@ -1,4 +1,3 @@
|
||||
// TODO move types
|
||||
import { BANK_INITIAL_REFRESH_TOKEN } from "@/../config.ts"
|
||||
import logger from "@/logger.ts"
|
||||
import dayjs from "dayjs"
|
||||
@ -9,7 +8,6 @@ import {
|
||||
type TokenResponse,
|
||||
} from "@/bank/db/queries.ts"
|
||||
import * as Api from "./sparebank1Api.ts"
|
||||
import { toISODateString } from "@/date.ts"
|
||||
|
||||
export interface OAuthTokenResponse {
|
||||
access_token: string
|
||||
@ -39,12 +37,11 @@ export type Bank = Sparebank1
|
||||
|
||||
export interface Sparebank1 {
|
||||
transactionsPastDay: (
|
||||
accountKeys: ReadonlyArray<string> | string,
|
||||
...accountKeys: ReadonlyArray<string>
|
||||
) => Promise<TransactionResponse>
|
||||
}
|
||||
|
||||
export class Sparebank1Impl implements Sparebank1 {
|
||||
private static baseUrl = "https://api.sparebank1.no"
|
||||
private readonly db: Database
|
||||
|
||||
constructor(db: Database) {
|
||||
@ -80,7 +77,6 @@ export class Sparebank1Impl implements Sparebank1 {
|
||||
|
||||
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") {
|
||||
@ -95,35 +91,13 @@ export class Sparebank1Impl implements Sparebank1 {
|
||||
}
|
||||
|
||||
async transactionsPastDay(
|
||||
accountKeys: ReadonlyArray<string> | string,
|
||||
...accountKeys: ReadonlyArray<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),
|
||||
return await Api.transactions(await this.getAccessToken(), accountKeys, {
|
||||
fromDate: lastDay,
|
||||
toDate: 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: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { BANK_OAUTH_CLIENT_ID, BANK_OAUTH_CLIENT_SECRET } from "../../config.ts"
|
||||
import { OAuthTokenResponse } from "@/bank/sparebank1.ts"
|
||||
import type {
|
||||
OAuthTokenResponse,
|
||||
TransactionResponse,
|
||||
} from "@/bank/sparebank1.ts"
|
||||
import logger from "@/logger.ts"
|
||||
import { type Dayjs } from "dayjs"
|
||||
import { toISODateString } from "@/date.ts"
|
||||
|
||||
const baseUrl = "https://api.sparebank1.no"
|
||||
|
||||
@ -16,6 +21,40 @@ function failure<T>(data: T): Failure<T> {
|
||||
return { status: "failure", data: data }
|
||||
}
|
||||
|
||||
export async function transactions(
|
||||
accessToken: string,
|
||||
accountKeys: string | ReadonlyArray<string>,
|
||||
timePeriod?: {
|
||||
fromDate: Dayjs
|
||||
toDate: Dayjs
|
||||
},
|
||||
): Promise<TransactionResponse> {
|
||||
const queries = new URLSearchParams({
|
||||
// TODO allow multiple accountKeys
|
||||
accountKey: typeof accountKeys === "string" ? accountKeys : accountKeys[0],
|
||||
...(timePeriod && {
|
||||
fromDate: toISODateString(timePeriod.fromDate),
|
||||
toDate: toISODateString(timePeriod.toDate),
|
||||
}),
|
||||
})
|
||||
|
||||
const url = `${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: [] }
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshToken(
|
||||
refreshToken: string,
|
||||
): Promise<Result<OAuthTokenResponse, string>> {
|
||||
@ -26,7 +65,7 @@ export async function refreshToken(
|
||||
grant_type: "refresh_token",
|
||||
})
|
||||
const url = `${baseUrl}/oauth/token?${queries}`
|
||||
logger.debug("Sending POST request to url: '%s'", url)
|
||||
logger.debug(`Sending POST request to url: '${url}'`)
|
||||
const response = await fetch(url, {
|
||||
method: "post",
|
||||
headers: {
|
||||
|
22
src/main.ts
22
src/main.ts
@ -16,6 +16,7 @@ import logger from "@/logger.ts"
|
||||
import type { UUID } from "node:crypto"
|
||||
import { createDb } from "@/bank/db/queries.ts"
|
||||
import * as fs from "node:fs"
|
||||
import { CronJob } from "cron"
|
||||
|
||||
// TODO Transports api for pino https://github.com/pinojs/pino/blob/HEAD/docs/transports.md
|
||||
|
||||
@ -27,12 +28,15 @@ export async function daily(actual: Actual, bank: Bank): Promise<void> {
|
||||
// TODO multiple accounts
|
||||
const accountId = ACTUAL_ACCOUNT_IDS[0] as UUID
|
||||
const actualTransactions = transactions.map((transaction) =>
|
||||
// TODO move to Bank interface?
|
||||
bankTransactionIntoActualTransaction(transaction, accountId),
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
`Mapped ${JSON.stringify(transactions)} to ${JSON.stringify(actualTransactions)} transactions`,
|
||||
)
|
||||
logger.trace({
|
||||
aMessage: "Mapped from Bank to Actual",
|
||||
from: JSON.stringify(transactions),
|
||||
to: JSON.stringify(actualTransactions),
|
||||
})
|
||||
|
||||
// TODO Import transactions into Actual
|
||||
// If multiple accounts, loop over them
|
||||
@ -48,7 +52,7 @@ export async function daily(actual: Actual, bank: Bank): Promise<void> {
|
||||
async function fetchTransactionsFromPastDay(
|
||||
bank: Bank,
|
||||
): Promise<ReadonlyArray<Transaction>> {
|
||||
const response = await bank.transactionsPastDay(BANK_ACCOUNT_IDS)
|
||||
const response = await bank.transactionsPastDay(...BANK_ACCOUNT_IDS)
|
||||
return response.transactions
|
||||
}
|
||||
|
||||
@ -68,22 +72,24 @@ async function main(): Promise<void> {
|
||||
const databaseFileName = `${DB_FILENAME}.sqlite`
|
||||
const db = createDb(databaseFileName)
|
||||
logger.info(`Started SQLlite database with filename="${databaseFileName}"`)
|
||||
const bank = new Sparebank1Impl(db)
|
||||
|
||||
process.on("SIGINT", async () => {
|
||||
logger.info("Caught interrupt signal")
|
||||
await shutdown()
|
||||
})
|
||||
|
||||
let cronJob: CronJob | undefined
|
||||
if (process.env.ONCE) {
|
||||
await daily(actual, new Sparebank1Impl(db))
|
||||
await daily(actual, bank)
|
||||
await shutdown()
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("Waiting for CRON job to start")
|
||||
const cronJob = cronJobDaily(async () => {
|
||||
cronJob = cronJobDaily(async () => {
|
||||
logger.info("Running daily job")
|
||||
await daily(actual, new Sparebank1Impl(db))
|
||||
await daily(actual, bank)
|
||||
logger.info("Finished daily job")
|
||||
})
|
||||
|
||||
@ -91,7 +97,7 @@ async function main(): Promise<void> {
|
||||
logger.info("Shutting down")
|
||||
await actual.shutdown()
|
||||
db.close()
|
||||
cronJob.stop()
|
||||
cronJob?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user