phase-8:server code enhancements.

This commit is contained in:
Kazuma
2026-06-05 22:44:04 -04:00
committed by Kazuma
parent 7cb1b03fd5
commit 7f2108129a
15 changed files with 781 additions and 94 deletions
+12 -12
View File
@@ -24,7 +24,7 @@ export class SimpleFINClient {
return;
}
if (process.env.SIMPLEFIN_SETUP_TOKEN) {
this.accessUrl = await this._claimAccessUrl(process.env.SIMPLEFIN_SETUP_TOKEN);
this.accessUrl = await this.claimAccessUrl(process.env.SIMPLEFIN_SETUP_TOKEN);
if (this.onAccessUrlClaimed) await this.onAccessUrlClaimed(this.accessUrl);
return;
}
@@ -36,7 +36,7 @@ export class SimpleFINClient {
async getAccounts(options: GetAccountsOptions = {}): Promise<SimpleFINData> {
if (!this.accessUrl) await this.init();
const startDate = options.startDate ?? this._daysAgo(30);
const startDate = options.startDate ?? this.daysAgo(30);
const endDate = options.endDate ?? Math.floor(Date.now() / 1000);
const parsed = new URL(this.accessUrl!);
@@ -59,13 +59,13 @@ export class SimpleFINClient {
data.errors.forEach((e) => this.logger.warn(` ⚠ SimpleFIN: ${e}`));
}
return this._normalise(data as { accounts: unknown[]; errors: string[] });
return this.normalise(data as { accounts: unknown[]; errors: string[] });
}
private async _claimAccessUrl(setupToken: string): Promise<string> {
private async claimAccessUrl(setupToken: string): Promise<string> {
const claimUrl = Buffer.from(setupToken.trim(), 'base64').toString('utf8').trim();
this.logger.write(`\n🔑 Claiming SimpleFIN access URL...\n → ${claimUrl}\n`);
const accessUrl = await this._post(claimUrl);
const accessUrl = await this.post(claimUrl);
if (!accessUrl || !accessUrl.startsWith('http')) {
throw new Error(
`Unexpected response from SimpleFIN: "${accessUrl}"\nSetup tokens are one-time use — if already claimed, generate a new one at https://beta-bridge.simplefin.org`,
@@ -75,7 +75,7 @@ export class SimpleFINClient {
return accessUrl.trim();
}
private _post(url: string): Promise<string> {
private post(url: string): Promise<string> {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const lib = parsed.protocol === 'https:' ? https : http;
@@ -101,7 +101,7 @@ export class SimpleFINClient {
});
}
private _normalise(data: { accounts: unknown[]; errors: string[] }): SimpleFINData {
private normalise(data: { accounts: unknown[]; errors: string[] }): SimpleFINData {
const accounts = (data.accounts ?? []).map((acc: any) => ({
id: acc.id,
name: acc.name,
@@ -109,19 +109,19 @@ export class SimpleFINClient {
balance: parseFloat(acc.balance) ?? 0,
balanceDate: new Date(acc['balance-date'] * 1000).toISOString().slice(0, 10),
org: acc.org?.name ?? 'Unknown',
type: this._classifyAccount(acc.name),
type: this.classifyAccount(acc.name),
transactions: (acc.transactions ?? []).map((tx: any) => ({
id: tx.id,
date: new Date(tx.posted * 1000).toISOString().slice(0, 10),
amount: parseFloat(tx.amount) ?? 0,
description: tx.description ?? '',
category: this._categorise(tx.description ?? ''),
category: this.categorise(tx.description ?? ''),
})),
}));
return { accounts, errors: data.errors ?? [] };
}
private _classifyAccount(name: string): string {
private classifyAccount(name: string): string {
const n = name.toLowerCase();
if (n.includes('checking') || n.includes('current')) return 'CHECKING';
if (n.includes('saving')) return 'SAVINGS';
@@ -132,7 +132,7 @@ export class SimpleFINClient {
return 'OTHER';
}
private _categorise(description: string): string {
private categorise(description: string): string {
const d = description.toLowerCase();
if (d.match(/amazon|walmart|target|costco|grocery|whole foods|trader joe/)) return 'Shopping';
if (d.match(/uber eats|doordash|grubhub|postmates|instacart/)) return 'Delivery';
@@ -147,7 +147,7 @@ export class SimpleFINClient {
return 'Other';
}
private _daysAgo(n: number): number {
private daysAgo(n: number): number {
return Math.floor((Date.now() - n * 24 * 60 * 60 * 1000) / 1000);
}
}