import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import { MarketCallRepository } from '../../domains/shared'; import { CalendarService } from './CalendarService'; import { ScreenerEngine } from '../screener'; import type { SnapshotEntry } from '../../domains/shared'; import { callSchema } from '../../domains/shared/types/schemas'; export class CallsController { constructor( private readonly repo: MarketCallRepository, private readonly engine: ScreenerEngine, private readonly calendar: CalendarService, ) {} private static toSnapshot(r: any): SnapshotEntry | null { if (!r) return null; const m = r.asset?.displayMetrics ?? r.asset?.getDisplayMetrics?.() ?? {}; return { price: r.asset?.currentPrice ?? null, signal: r.signal ?? null, inflatedVerdict: r.inflated?.label ?? null, fundamentalVerdict: r.fundamental?.label ?? null, pe: m['P/E'] ?? null, roe: m['ROE%'] ?? null, fcf: m['FCF Yld%'] ?? null, }; } register(app: FastifyInstance): void { app.get('/api/calls', this.list.bind(this)); app.get('/api/calls/calendar', this.handleCalendar.bind(this)); app.get('/api/calls/:id', this.get.bind(this)); app.post('/api/calls', { schema: callSchema }, this.create.bind(this)); app.delete('/api/calls/:id', this.remove.bind(this)); } private async list() { return { calls: this.repo.list() }; } private async get(req: FastifyRequest, reply: FastifyReply) { const call = this.repo.get((req.params as { id: string }).id); if (!call) return reply.code(404).send({ error: 'Call not found' }); const current: Record = {}; if (call.tickers.length > 0) { try { const results = await this.engine.screenTickers(call.tickers); for (const r of [...results.STOCK, ...results.ETF, ...results.BOND]) { current[r.asset.ticker] = CallsController.toSnapshot(r); } } catch { /* non-fatal */ } } return { ...call, current }; } private async create(req: FastifyRequest, reply: FastifyReply) { const { title, quarter, date, thesis, tickers } = req.body as { title: string; quarter: string; date?: string; thesis: string; tickers: string[]; }; const upperTickers = tickers.map((t) => t.toUpperCase()); const snapshot: Record = {}; try { const results = await this.engine.screenTickers(upperTickers); for (const r of [...results.STOCK, ...results.ETF, ...results.BOND]) { snapshot[r.asset.ticker] = CallsController.toSnapshot(r); } } catch (err) { req.log.warn(`Could not snapshot prices for market call: ${(err as Error).message}`); } const call = this.repo.create({ title, quarter, date, thesis, tickers: upperTickers, snapshot: snapshot as any, }); return reply.code(201).send(call); } private async remove(req: FastifyRequest, reply: FastifyReply) { const deleted = this.repo.delete((req.params as { id: string }).id); if (!deleted) return reply.code(404).send({ error: 'Call not found' }); return { ok: true }; } private async handleCalendar(req: FastifyRequest) { let tickers: string[]; if ((req.query as any).tickers) { tickers = String((req.query as any).tickers) .split(',') .map((t) => t.trim().toUpperCase()) .filter(Boolean); } else { tickers = [...new Set(this.repo.list().flatMap((c) => c.tickers))]; } return this.calendar.getEvents(tickers); } }