phase-10.5: market screener ui enhancements
This commit is contained in:
@@ -1,26 +1,59 @@
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { SimpleFINClient, PortfolioRepository, noopLogger } from '../../domains/shared';
|
||||
import { PersonalFinanceAnalyzer, ScreenerEngine } from '../screener';
|
||||
import { PortfolioAdvisor } from '../portfolio/PortfolioAdvisor';
|
||||
import type { PortfolioHolding } from '../../domains/shared';
|
||||
import { holdingSchema } from '../../domains/shared/types/schemas';
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest, preHandlerHookHandler } from 'fastify';
|
||||
import { SimpleFINClient, PortfolioRepository, noopLogger } from '../../domains/shared/index.js';
|
||||
import { PersonalFinanceAnalyzer, ScreenerEngine } from '../screener/index.js';
|
||||
import { PortfolioAdvisor } from '../portfolio/PortfolioAdvisor.js';
|
||||
import type { PortfolioHolding } from '../../domains/shared/index.js';
|
||||
import { holdingSchema } from '../../domains/shared/types/schemas.js';
|
||||
import type { TokenPayload } from '../auth/index.js';
|
||||
|
||||
interface FinanceControllerOptions {
|
||||
authGuard?: preHandlerHookHandler;
|
||||
traderGuard?: preHandlerHookHandler;
|
||||
}
|
||||
|
||||
type AuthRequest = FastifyRequest & { user?: TokenPayload };
|
||||
|
||||
function userId(req: FastifyRequest): string {
|
||||
return (req as AuthRequest).user?.sub ?? '';
|
||||
}
|
||||
|
||||
export class FinanceController {
|
||||
// All portfolio routes only need a valid login — data is already user-scoped by user_id.
|
||||
// No role restriction needed; any registered user can manage their own portfolio.
|
||||
readonly #authGuards: preHandlerHookHandler[];
|
||||
|
||||
constructor(
|
||||
private readonly engine: ScreenerEngine,
|
||||
private readonly repo: PortfolioRepository,
|
||||
private readonly advisor: PortfolioAdvisor,
|
||||
) {}
|
||||
options: FinanceControllerOptions = {},
|
||||
) {
|
||||
this.#authGuards = options.authGuard ? [options.authGuard] : [];
|
||||
}
|
||||
|
||||
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/portfolio', { preHandler: this.#authGuards }, this.portfolio.bind(this));
|
||||
app.post(
|
||||
'/api/finance/holdings',
|
||||
{
|
||||
schema: holdingSchema,
|
||||
preHandler: this.#authGuards,
|
||||
},
|
||||
this.addHolding.bind(this),
|
||||
);
|
||||
app.delete(
|
||||
'/api/finance/holdings/:ticker',
|
||||
{
|
||||
preHandler: this.#authGuards,
|
||||
},
|
||||
this.removeHolding.bind(this),
|
||||
);
|
||||
app.get('/api/finance/market-context', this.marketContext.bind(this));
|
||||
}
|
||||
|
||||
private async portfolio(_req: FastifyRequest, _reply: FastifyReply) {
|
||||
const { holdings } = this.repo.exists() ? this.repo.read() : { holdings: [] };
|
||||
private async portfolio(req: FastifyRequest, _reply: FastifyReply) {
|
||||
const uid = userId(req);
|
||||
const { holdings } = this.repo.exists(uid) ? this.repo.read(uid) : { holdings: [] };
|
||||
|
||||
let personalFinance = null;
|
||||
if (process.env.SIMPLEFIN_ACCESS_URL) {
|
||||
@@ -43,6 +76,7 @@ export class FinanceController {
|
||||
}
|
||||
|
||||
private async addHolding(req: FastifyRequest, reply: FastifyReply) {
|
||||
const uid = userId(req);
|
||||
const {
|
||||
ticker,
|
||||
shares,
|
||||
@@ -50,14 +84,14 @@ export class FinanceController {
|
||||
type = 'stock',
|
||||
source = 'Manual',
|
||||
} = req.body as PortfolioHolding;
|
||||
const entry = this.repo.upsert({ ticker, shares, costBasis, type, source });
|
||||
const entry = this.repo.upsert({ ticker, shares, costBasis, type, source }, uid);
|
||||
return reply.code(201).send(entry);
|
||||
}
|
||||
|
||||
private async removeHolding(req: FastifyRequest, reply: FastifyReply) {
|
||||
const uid = userId(req);
|
||||
const ticker = (req.params as { ticker: string }).ticker.toUpperCase();
|
||||
|
||||
const removed = this.repo.remove(ticker);
|
||||
const removed = this.repo.remove(ticker, uid);
|
||||
if (!removed) return reply.code(404).send({ error: 'Holding not found' });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user