import test from 'node:test'; import assert from 'node:assert/strict'; import { FinanceController } from '../server/domains/finance/finance.controller.js'; import { PortfolioAdvisor } from '../server/domains/portfolio/PortfolioAdvisor.js'; import type { PortfolioHolding } from '../server/domains/shared/types/portfolio.model.js'; class MockScreenerEngine { async screenTickers() { return { STOCK: [], ETF: [], BOND: [], ERROR: [], marketContext: { sp500Price: 5500, riskFreeRate: 4.5, vixLevel: 12.5, rateRegime: 'NORMAL' as const, volatilityRegime: 'LOW' as const, benchmarks: { marketPE: 20, techPE: 28, reitYield: 3.5, igSpread: 1.2, }, }, }; } } class MockPortfolioRepository { async getHoldings() { return [ { id: '1', ticker: 'AAPL', shares: 10, costBasis: 150, type: 'stock' as const, }, ]; } async addHolding(holding: PortfolioHolding) { return { id: '1', ...holding }; } async deleteHolding(_id: string) { return true; } } class MockYahooClient { async fetchSummary() { return { price: { regularMarketPrice: 189.5 }, summaryDetail: { fiftyTwoWeekHigh: 199, fiftyTwoWeekLow: 164 }, quoteType: { quoteType: 'EQUITY' }, defaultKeyStatistics: { trailingPE: 28.5 }, financialData: { returnOnEquity: 95.2 / 100, operatingMargins: 30.7 / 100, grossMargins: 46.2 / 100, freeCashflow: 110e9, totalRevenue: 383.3e9, debtToEquity: 75.56, currentRatio: 0.95, }, incomeStatementHistoryQuarterly: { incomeStatementHistory: [ { commonStockSharesOutstanding: 15.6e9, netIncome: 25e9, }, ], }, }; } } test('FinanceController', async (t) => { await t.test('registers portfolio endpoint', async () => { const engine = new MockScreenerEngine() as any; const repository = new MockPortfolioRepository() as any; const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const controller = new FinanceController(engine, repository, advisor); let portfolioEndpointRegistered = false; const mockApp = { get: (path: string) => { if (path === '/api/finance/portfolio') portfolioEndpointRegistered = true; }, post: () => {}, delete: () => {}, }; controller.register(mockApp as any); assert.ok(portfolioEndpointRegistered); }); await t.test('registers market context endpoint', async () => { const engine = new MockScreenerEngine() as any; const repository = new MockPortfolioRepository() as any; const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const controller = new FinanceController(engine, repository, advisor); let contextEndpointRegistered = false; const mockApp = { get: (path: string) => { if (path === '/api/finance/market-context') contextEndpointRegistered = true; }, post: () => {}, delete: () => {}, }; controller.register(mockApp as any); assert.ok(contextEndpointRegistered); }); await t.test('registers holdings endpoints', async () => { const engine = new MockScreenerEngine() as any; const repository = new MockPortfolioRepository() as any; const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const controller = new FinanceController(engine, repository, advisor); let addHoldingRegistered = false; let deleteHoldingRegistered = false; const mockApp = { get: () => {}, post: (path: string) => { if (path === '/api/finance/holdings') addHoldingRegistered = true; }, delete: (path: string) => { if (path === '/api/finance/holdings/:ticker') deleteHoldingRegistered = true; }, }; controller.register(mockApp as any); assert.ok(addHoldingRegistered); assert.ok(deleteHoldingRegistered); }); await t.test('returns portfolio with holdings', async () => { const engine = new MockScreenerEngine() as any; const repository = new MockPortfolioRepository() as any; const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const _controller = new FinanceController(engine, repository, advisor); const holdings = await repository.getHoldings(); assert.ok(Array.isArray(holdings)); assert.equal(holdings.length, 1); assert.equal(holdings[0].ticker, 'AAPL'); }); await t.test('returns market context data', async () => { const engine = new MockScreenerEngine() as any; const repository = new MockPortfolioRepository() as any; const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const _controller = new FinanceController(engine, repository, advisor); const results = await engine.screenTickers([]); assert.ok(results.marketContext); assert.ok(results.marketContext.sp500Price); assert.ok(results.marketContext.benchmarks); }); await t.test('adds holding to portfolio', async () => { const engine = new MockScreenerEngine() as any; const repository = new MockPortfolioRepository() as any; const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const _controller = new FinanceController(engine, repository, advisor); const newHolding: PortfolioHolding = { ticker: 'MSFT', shares: 5, costBasis: 400, source: 'manual', type: 'stock', }; const added = await repository.addHolding(newHolding); assert.ok(added); assert.equal(added.ticker, 'MSFT'); }); await t.test('deletes holding from portfolio', async () => { const engine = new MockScreenerEngine() as any; const repository = new MockPortfolioRepository() as any; const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const _controller = new FinanceController(engine, repository, advisor); const deleted = await repository.deleteHolding('1'); assert.ok(deleted); }); await t.test('handles empty portfolio gracefully', async () => { class EmptyRepository { async getHoldings() { return []; } async addHolding(holding: PortfolioHolding) { return { id: '1', ...holding }; } async deleteHolding(_id: string) { return true; } } const engine = new MockScreenerEngine() as any; const repository = new EmptyRepository() as any; const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const _controller = new FinanceController(engine, repository, advisor); const holdings = await repository.getHoldings(); assert.equal(holdings.length, 0); }); await t.test('analyzes portfolio advice', async () => { const engine = new MockScreenerEngine() as any; const repository = new MockPortfolioRepository() as any; const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const _controller = new FinanceController(engine, repository, advisor); const holdings = await repository.getHoldings(); const advice = await advisor.advise(holdings, { STOCK: [], ETF: [], BOND: [], ERROR: [], marketContext: null as any, }); assert.ok(advice); }); await t.test('includes holdings in portfolio response', async () => { const engine = new MockScreenerEngine() as any; const repository = new MockPortfolioRepository() as any; const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const _controller = new FinanceController(engine, repository, advisor); const holdings = await repository.getHoldings(); const advice = await advisor.advise(holdings, { STOCK: [], ETF: [], BOND: [], ERROR: [], marketContext: null as any, }); assert.ok(holdings); assert.ok(advice); }); await t.test('handles multiple holdings in portfolio', async () => { class MultiHoldingRepository { async getHoldings() { return [ { id: '1', ticker: 'AAPL', shares: 10, costBasis: 150, type: 'stock' as const, }, { id: '2', ticker: 'MSFT', shares: 5, costBasis: 400, type: 'stock' as const, }, { id: '3', ticker: 'VOO', shares: 20, costBasis: 350, type: 'etf' as const, }, ]; } async addHolding(holding: PortfolioHolding) { return { id: '4', ...holding }; } async deleteHolding(_id: string) { return true; } } const engine = new MockScreenerEngine() as any; const repository = new MultiHoldingRepository() as any; const advisor = new PortfolioAdvisor(new MockYahooClient() as any); const _controller = new FinanceController(engine, repository, advisor); const holdings = await repository.getHoldings(); assert.equal(holdings.length, 3); assert.ok(holdings.some((h: any) => h.ticker === 'AAPL')); assert.ok(holdings.some((h: any) => h.ticker === 'MSFT')); assert.ok(holdings.some((h: any) => h.type === 'etf')); }); });