aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/lib')
-rw-r--r--app/src/lib/helpers.ts5
-rw-r--r--app/src/lib/server/actual.ts23
-rw-r--r--app/src/lib/server/db/schema.ts2
-rw-r--r--app/src/lib/server/importer.ts9
-rw-r--r--app/src/lib/server/sb1.ts85
-rw-r--r--app/src/lib/server/session-log.ts14
-rw-r--r--app/src/lib/ui/button.svelte4
7 files changed, 118 insertions, 24 deletions
diff --git a/app/src/lib/helpers.ts b/app/src/lib/helpers.ts
new file mode 100644
index 0000000..35b5f65
--- /dev/null
+++ b/app/src/lib/helpers.ts
@@ -0,0 +1,5 @@
+import type { Temporal } from "temporal-polyfill";
+
+export function instantAsHtmlInputValueString(instant: Temporal.Instant) {
+ return instant.toString().split("T")[0]
+} \ No newline at end of file
diff --git a/app/src/lib/server/actual.ts b/app/src/lib/server/actual.ts
index 7291aad..0875273 100644
--- a/app/src/lib/server/actual.ts
+++ b/app/src/lib/server/actual.ts
@@ -1,21 +1,28 @@
-import { ACTUAL_HOST, ACTUAL_PASS } from "$env/static/private";
+import { ACTUAL_BUDGET_ID, ACTUAL_HOST, ACTUAL_PASS } from "$env/static/private";
import * as actual from "@actual-app/api"
import { existsSync, mkdirSync } from "node:fs";
-import path from "node:path";
+import path from "node:path"
+import process from "node:process";
async function init_actual() {
- const dataDir = path.resolve(import.meta.dirname, "actualDataDir");
-
- if (!existsSync(dataDir)) mkdirSync(dataDir);
-
+ const dataDir = path.resolve(process.cwd(), "data/actualDataDir")
+ if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
return actual.init({
password: ACTUAL_PASS,
serverURL: ACTUAL_HOST,
dataDir: dataDir
+ }).then(async () => {
+ await actual.downloadBudget(ACTUAL_BUDGET_ID)
+ await actual.sync()
})
}
export async function get_budgets() {
await init_actual()
- return await actual.getBudgets()
-} \ No newline at end of file
+ return actual.getBudgets()
+}
+
+export async function get_accounts() {
+ await init_actual()
+ return actual.getAccounts()
+}
diff --git a/app/src/lib/server/db/schema.ts b/app/src/lib/server/db/schema.ts
index bb57703..4d13ed6 100644
--- a/app/src/lib/server/db/schema.ts
+++ b/app/src/lib/server/db/schema.ts
@@ -1,6 +1,7 @@
import { relations, sql } from 'drizzle-orm';
import { numeric, text, pgTable, uuid, json } from "drizzle-orm/pg-core";
import type { Sb1Tokens } from '../sb1';
+import type { SessionLogType } from '../session-log';
export const syncSession = pgTable("session", {
id: uuid('id').primaryKey().default(sql`uuidv7()`),
@@ -14,6 +15,7 @@ export const syncLog = pgTable("session_log", {
id: uuid('id').primaryKey().default(sql`uuidv7()`),
sessionId: text("session_id"),
dateTime: text("date_time"),
+ type: text("type").$type<SessionLogType>(),
msg: text("msg")
})
diff --git a/app/src/lib/server/importer.ts b/app/src/lib/server/importer.ts
new file mode 100644
index 0000000..91adce0
--- /dev/null
+++ b/app/src/lib/server/importer.ts
@@ -0,0 +1,9 @@
+import type { Temporal } from "temporal-polyfill";
+import sb1 from "./sb1";
+
+async function importSince(account: string, date: Temporal.Instant) {
+ const accounts = await sb1.data.get_accounts()
+ for (const account of accounts?.accounts ?? []) {
+ const transactions = await sb1.data.get_transactions(account.key);
+ }
+} \ No newline at end of file
diff --git a/app/src/lib/server/sb1.ts b/app/src/lib/server/sb1.ts
index a7cad3e..f6507ef 100644
--- a/app/src/lib/server/sb1.ts
+++ b/app/src/lib/server/sb1.ts
@@ -4,6 +4,7 @@ import { Temporal } from "temporal-polyfill";
import { randomUUID } from "node:crypto";
import { db } from "./db";
import { syncSession } from "./db/schema";
+import { add_session_log } from "./session-log";
export type Sb1Tokens = {
access_token: string
@@ -14,12 +15,12 @@ export type Sb1Tokens = {
refresh_token: string
}
-export type Transaction = {
+export type Sb1Transaction = {
id: string
nonUniqueId: string
description: string
cleanedDescription: string
- accountNumber: AccountNumber
+ accountNumber: Sb1AccountNumber
amount: number
date: number
interestDate: number
@@ -34,16 +35,44 @@ export type Transaction = {
accountKey: string
accountCurrency: string
isFromCurrencyAccount: boolean
- classificationInput: ClassificationInput
+ classificationInput: Sb1ClassificationInput
}
-export type AccountNumber = {
+export type Sb1Account = {
+ key: string;
+ accountNumber: string;
+ iban: string;
+ name: string;
+ description: string;
+ balance: number;
+ availableBalance: number;
+ currencyCode: string;
+ owner: Sb1AccountOwner;
+ productType: string;
+ type: string;
+ productId: string;
+ descriptionCode: string;
+ accountProperties: { [key: string]: boolean };
+}
+
+export type Sb1AccountOwner = {
+ name: string;
+ firstName: string;
+ lastName: string;
+ type: string;
+ age: number;
+ customerKey: string;
+ ssnKey: string;
+}
+
+
+export type Sb1AccountNumber = {
value: string
formatted: string
unformatted: string
}
-export type ClassificationInput = {
+export type Sb1ClassificationInput = {
id: string
amount: number
type: string
@@ -139,12 +168,13 @@ const auth = {
const epoch = Temporal.Now.instant().epochMilliseconds
if (res.ok) {
await db.update(syncSession).set({ tokens, accessTokenCreated: epoch.toString(), refreshTokenCreated: epoch.toString() }).where(eq(syncSession.id, id))
+ await add_session_log(id, "REFRESH_SB1_TOKEN", "Done")
return tokens
} else {
console.error("Failed to refresh tokens", tokens)
+ await add_session_log(id, "REFRESH_SB1_TOKEN", "Failed: " + JSON.stringify(res))
return null
}
-
}
}
@@ -160,23 +190,50 @@ const data = {
Authorization: `Bearer ${token}`
}
})
- if (response.ok) return await response.json() as { accounts: Array<any> }
+ if (response.ok) return await response.json() as { accounts: Array<Sb1Account> }
else console.error(await response.text())
},
- async get_transactions(accountKey: string) {
+ async get_transactions(accountKey: string, delta?: Temporal.Instant) {
const token = await auth.get_access_token()
+
if (!token) return undefined
- const response = await fetch("https://api.sparebank1.no/personal/banking/transactions?" + new URLSearchParams({
- "accountKey": accountKey
- }), {
+ const params = new URLSearchParams({
+ "accountKey": accountKey,
+ });
+ if (delta) params.append("fromDate", formatInstant(delta, "yyyy-MM-dd"))
+ const response = await fetch("https://api.sparebank1.no/personal/banking/transactions?" + params, {
headers: {
Authorization: `Bearer ${token}`,
},
});
const json = await response.json()
- console.log(accountKey + ":" + json["transactions"]?.length)
- return json["transactions"] as Transaction[];
+ return json["transactions"] as Sb1Transaction[];
}
}
-export default { auth, data } \ No newline at end of file
+export default { auth, data }
+
+export function formatInstant(
+ instant: Temporal.Instant,
+ format: string,
+ timeZone: string = "UTC"
+): string {
+ const zdt = instant.toZonedDateTimeISO(timeZone);
+
+ const pad = (value: number, length = 2) =>
+ value.toString().padStart(length, "0");
+
+ const replacements: Record<string, string> = {
+ yyyy: pad(zdt.year, 4),
+ MM: pad(zdt.month),
+ dd: pad(zdt.day),
+ HH: pad(zdt.hour),
+ mm: pad(zdt.minute),
+ ss: pad(zdt.second),
+ };
+
+ return format.replace(
+ /yyyy|MM|dd|HH|mm|ss/g,
+ (token) => replacements[token]
+ );
+}
diff --git a/app/src/lib/server/session-log.ts b/app/src/lib/server/session-log.ts
new file mode 100644
index 0000000..1195621
--- /dev/null
+++ b/app/src/lib/server/session-log.ts
@@ -0,0 +1,14 @@
+import { Temporal } from "temporal-polyfill"
+import { db } from "./db"
+import { syncLog } from "./db/schema"
+
+export type SessionLogType = "CREATED" | "SYNC_START" | "REFRESH_SB1_TOKEN"
+
+export async function add_session_log(id: string, type: SessionLogType, msg: string) {
+ db.insert(syncLog).values({
+ dateTime: String(Temporal.Now.instant().epochMilliseconds),
+ sessionId: id,
+ type: type,
+ msg: msg
+ })
+} \ No newline at end of file
diff --git a/app/src/lib/ui/button.svelte b/app/src/lib/ui/button.svelte
index 37b1c97..ad82f57 100644
--- a/app/src/lib/ui/button.svelte
+++ b/app/src/lib/ui/button.svelte
@@ -24,7 +24,7 @@
cursor: pointer;
display: flex;
gap: 3px;
- transition: 0.1s all ease;
+ transition: 0.075s all ease;
height: fit-content;
&:hover,
@@ -35,7 +35,7 @@
&:active {
background-color: rgba(0, 0, 0, 0.2);
transform: scale(0.96);
- transition: 0.2s all ease;
+ transition: 0.15s all ease;
}
}
</style>