phase-7: code restructure

This commit is contained in:
Kazuma
2026-06-05 22:05:55 -04:00
committed by Kazuma
parent 2b785aa861
commit 5b32bd7a04
108 changed files with 8931 additions and 3434 deletions
+75
View File
@@ -0,0 +1,75 @@
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { SimpleFINClient } from '../clients/SimpleFINClient';
import { PortfolioRepository } from '../repositories/PortfolioRepository';
import { PersonalFinanceAnalyzer, PortfolioAdvisor, ScreenerEngine } from '../services/index';
import type { PortfolioHolding } from '../types';
import { holdingSchema } from '../types/schemas';
import { noopLogger } from '../utils/logger';
export class FinanceController {
constructor(
private readonly engine: ScreenerEngine,
private readonly repo: PortfolioRepository,
) {}
private static normalizeYahoo(ticker: string): string {
return ticker.toUpperCase().replace(/\./g, '-');
}
register(app: FastifyInstance): void {
app.get('/api/finance/portfolio', this.portfolio.bind(this));
app.post('/api/finance/holdings', { schema: holdingSchema }, this.addHolding.bind(this));
app.delete('/api/finance/holdings/:ticker', this.removeHolding.bind(this));
app.get('/api/finance/market-context', this.marketContext.bind(this));
}
private async portfolio(_req: FastifyRequest, reply: FastifyReply) {
if (!this.repo.exists()) return reply.code(404).send({ error: 'portfolio.json not found' });
const { holdings } = this.repo.read();
let personalFinance = null;
if (process.env.SIMPLEFIN_ACCESS_URL) {
const client = new SimpleFINClient({ logger: noopLogger });
const { accounts } = await client.getAccounts();
personalFinance = new PersonalFinanceAnalyzer().analyze(accounts);
}
const screenable = holdings
.filter((h) => (h.type ?? 'stock') !== 'crypto')
.map((h) => FinanceController.normalizeYahoo(h.ticker));
const results =
screenable.length > 0
? await this.engine.screenTickers(screenable)
: { STOCK: [], ETF: [], BOND: [], ERROR: [], marketContext: {} as any };
const advice = await new PortfolioAdvisor().advise(holdings, results);
return { advice, personalFinance, marketContext: results.marketContext };
}
private async addHolding(req: FastifyRequest, reply: FastifyReply) {
const {
ticker,
shares,
costBasis = 0,
type = 'stock',
source = 'Manual',
} = req.body as PortfolioHolding;
const entry = this.repo.upsert({ ticker, shares, costBasis, type, source });
return reply.code(201).send(entry);
}
private async removeHolding(req: FastifyRequest, reply: FastifyReply) {
const ticker = (req.params as { ticker: string }).ticker.toUpperCase();
if (!this.repo.exists()) return reply.code(404).send({ error: 'portfolio.json not found' });
const removed = this.repo.remove(ticker);
if (!removed) return reply.code(404).send({ error: 'Holding not found' });
return { ok: true };
}
private async marketContext() {
return this.engine.getMarketContext();
}
}