import { Elysia, t } from 'elysia'; import type { Filing, FinancialHistoryWindow, FinancialStatementKind, FinancialStatementMode, TaskStatus } from '@/lib/types'; import { auth } from '@/lib/auth'; import { requireAuthenticatedSession } from '@/lib/server/auth-session'; import { asErrorMessage, jsonError } from '@/lib/server/http'; import { buildPortfolioSummary } from '@/lib/server/portfolio'; import { defaultFinancialSyncLimit, getCompanyFinancialStatements } from '@/lib/server/financial-statements'; import { redactInternalFilingAnalysisFields } from '@/lib/server/api/filing-redaction'; import { getFilingByAccession, listFilingsRecords } from '@/lib/server/repos/filings'; import { deleteHoldingByIdRecord, listUserHoldings, updateHoldingByIdRecord, upsertHoldingRecord } from '@/lib/server/repos/holdings'; import { getLatestPortfolioInsight } from '@/lib/server/repos/insights'; import { deleteWatchlistItemRecord, listWatchlistItems, upsertWatchlistItemRecord } from '@/lib/server/repos/watchlist'; import { getPriceHistory, getQuote } from '@/lib/server/prices'; import { enqueueTask, getTaskById, getTaskQueueSnapshot, listRecentTasks } from '@/lib/server/tasks'; const ALLOWED_STATUSES: TaskStatus[] = ['queued', 'running', 'completed', 'failed']; const FINANCIAL_FORMS: ReadonlySet = new Set(['10-K', '10-Q']); const AUTO_FILING_SYNC_LIMIT = 20; const FINANCIALS_V2_ENABLED = process.env.FINANCIALS_V2?.trim().toLowerCase() !== 'false'; const FINANCIAL_STATEMENT_MODES: FinancialStatementMode[] = ['standardized', 'filing_faithful']; const FINANCIAL_STATEMENT_KINDS: FinancialStatementKind[] = [ 'income', 'balance', 'cash_flow', 'equity', 'comprehensive_income' ]; const FINANCIAL_HISTORY_WINDOWS: FinancialHistoryWindow[] = ['10y', 'all']; function asRecord(value: unknown): Record { if (!value || typeof value !== 'object' || Array.isArray(value)) { return {}; } return value as Record; } function asPositiveNumber(value: unknown) { const parsed = typeof value === 'number' ? value : Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : null; } function asBoolean(value: unknown, fallback = false) { if (typeof value === 'boolean') { return value; } if (typeof value === 'string') { const normalized = value.trim().toLowerCase(); if (normalized === 'true' || normalized === '1' || normalized === 'yes') { return true; } if (normalized === 'false' || normalized === '0' || normalized === 'no') { return false; } } return fallback; } function asStatementMode(value: unknown): FinancialStatementMode { return FINANCIAL_STATEMENT_MODES.includes(value as FinancialStatementMode) ? value as FinancialStatementMode : 'standardized'; } function asStatementKind(value: unknown): FinancialStatementKind { return FINANCIAL_STATEMENT_KINDS.includes(value as FinancialStatementKind) ? value as FinancialStatementKind : 'income'; } function asHistoryWindow(value: unknown): FinancialHistoryWindow { return FINANCIAL_HISTORY_WINDOWS.includes(value as FinancialHistoryWindow) ? value as FinancialHistoryWindow : '10y'; } function withFinancialMetricsPolicy(filing: Filing): Filing { if (FINANCIAL_FORMS.has(filing.filing_type)) { return filing; } return { ...filing, metrics: null }; } async function queueAutoFilingSync(userId: string, ticker: string) { try { await enqueueTask({ userId, taskType: 'sync_filings', payload: { ticker, limit: AUTO_FILING_SYNC_LIMIT }, priority: 90 }); return true; } catch (error) { console.error(`[auto-filing-sync] failed for ${ticker}:`, error); return false; } } const authHandler = ({ request }: { request: Request }) => auth.handler(request); export const app = new Elysia({ prefix: '/api' }) .all('/auth', authHandler) .all('/auth/*', authHandler) .get('/health', async () => { const queue = await getTaskQueueSnapshot(); return Response.json({ status: 'ok', version: '4.0.0', timestamp: new Date().toISOString(), queue }); }) .get('/me', async () => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } return Response.json({ user: { id: session.user.id, email: session.user.email, name: session.user.name, image: session.user.image } }); }) .get('/watchlist', async () => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const items = await listWatchlistItems(session.user.id); return Response.json({ items }); }) .post('/watchlist', async ({ body }) => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const payload = asRecord(body); const ticker = typeof payload.ticker === 'string' ? payload.ticker.trim().toUpperCase() : ''; const companyName = typeof payload.companyName === 'string' ? payload.companyName.trim() : ''; const sector = typeof payload.sector === 'string' ? payload.sector.trim() : ''; if (!ticker) { return jsonError('ticker is required'); } if (!companyName) { return jsonError('companyName is required'); } try { const { item, created } = await upsertWatchlistItemRecord({ userId: session.user.id, ticker, companyName, sector }); const autoFilingSyncQueued = created ? await queueAutoFilingSync(session.user.id, ticker) : false; return Response.json({ item, autoFilingSyncQueued }); } catch (error) { return jsonError(asErrorMessage(error, 'Failed to create watchlist item')); } }, { body: t.Object({ ticker: t.String({ minLength: 1 }), companyName: t.String({ minLength: 1 }), sector: t.Optional(t.String()) }) }) .delete('/watchlist/:id', async ({ params }) => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const numericId = Number(params.id); if (!Number.isInteger(numericId) || numericId <= 0) { return jsonError('Invalid watchlist id', 400); } const removed = await deleteWatchlistItemRecord(session.user.id, numericId); if (!removed) { return jsonError('Watchlist item not found', 404); } return Response.json({ success: true }); }, { params: t.Object({ id: t.String({ minLength: 1 }) }) }) .get('/portfolio/holdings', async () => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const holdings = await listUserHoldings(session.user.id); return Response.json({ holdings }); }) .post('/portfolio/holdings', async ({ body }) => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const payload = asRecord(body); const ticker = typeof payload.ticker === 'string' ? payload.ticker.trim().toUpperCase() : ''; const shares = asPositiveNumber(payload.shares); const avgCost = asPositiveNumber(payload.avgCost); if (!ticker) { return jsonError('ticker is required'); } if (shares === null) { return jsonError('shares must be a positive number'); } if (avgCost === null) { return jsonError('avgCost must be a positive number'); } try { const currentPrice = asPositiveNumber(payload.currentPrice) ?? avgCost; const { holding, created } = await upsertHoldingRecord({ userId: session.user.id, ticker, shares, avgCost, currentPrice }); const autoFilingSyncQueued = created ? await queueAutoFilingSync(session.user.id, ticker) : false; return Response.json({ holding, autoFilingSyncQueued }); } catch (error) { return jsonError(asErrorMessage(error, 'Failed to save holding')); } }, { body: t.Object({ ticker: t.String({ minLength: 1 }), shares: t.Number({ exclusiveMinimum: 0 }), avgCost: t.Number({ exclusiveMinimum: 0 }), currentPrice: t.Optional(t.Number({ exclusiveMinimum: 0 })) }) }) .patch('/portfolio/holdings/:id', async ({ params, body }) => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const numericId = Number(params.id); if (!Number.isInteger(numericId) || numericId <= 0) { return jsonError('Invalid holding id'); } const payload = asRecord(body); const updated = await updateHoldingByIdRecord({ userId: session.user.id, id: numericId, shares: asPositiveNumber(payload.shares) ?? undefined, avgCost: asPositiveNumber(payload.avgCost) ?? undefined, currentPrice: asPositiveNumber(payload.currentPrice) ?? undefined }); if (!updated) { return jsonError('Holding not found', 404); } return Response.json({ holding: updated }); }, { params: t.Object({ id: t.String({ minLength: 1 }) }), body: t.Object({ shares: t.Optional(t.Number({ exclusiveMinimum: 0 })), avgCost: t.Optional(t.Number({ exclusiveMinimum: 0 })), currentPrice: t.Optional(t.Number({ exclusiveMinimum: 0 })) }) }) .delete('/portfolio/holdings/:id', async ({ params }) => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const numericId = Number(params.id); if (!Number.isInteger(numericId) || numericId <= 0) { return jsonError('Invalid holding id'); } const removed = await deleteHoldingByIdRecord(session.user.id, numericId); if (!removed) { return jsonError('Holding not found', 404); } return Response.json({ success: true }); }, { params: t.Object({ id: t.String({ minLength: 1 }) }) }) .get('/portfolio/summary', async () => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const holdings = await listUserHoldings(session.user.id); const summary = buildPortfolioSummary(holdings); return Response.json({ summary }); }) .post('/portfolio/refresh-prices', async () => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } try { const task = await enqueueTask({ userId: session.user.id, taskType: 'refresh_prices', payload: {}, priority: 80 }); return Response.json({ task }); } catch (error) { return jsonError(asErrorMessage(error, 'Failed to queue refresh task')); } }) .post('/portfolio/insights/generate', async () => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } try { const task = await enqueueTask({ userId: session.user.id, taskType: 'portfolio_insights', payload: {}, priority: 70 }); return Response.json({ task }); } catch (error) { return jsonError(asErrorMessage(error, 'Failed to queue insights task')); } }) .get('/portfolio/insights/latest', async () => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const insight = await getLatestPortfolioInsight(session.user.id); return Response.json({ insight }); }) .get('/analysis/company', async ({ query }) => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const ticker = typeof query.ticker === 'string' ? query.ticker.trim().toUpperCase() : ''; if (!ticker) { return jsonError('ticker is required'); } const [filings, holdings, watchlist, liveQuote, priceHistory] = await Promise.all([ listFilingsRecords({ ticker, limit: 40 }), listUserHoldings(session.user.id), listWatchlistItems(session.user.id), getQuote(ticker), getPriceHistory(ticker) ]); const redactedFilings = filings .map(redactInternalFilingAnalysisFields) .map(withFinancialMetricsPolicy); const latestFiling = redactedFilings[0] ?? null; const holding = holdings.find((entry) => entry.ticker === ticker) ?? null; const watchlistItem = watchlist.find((entry) => entry.ticker === ticker) ?? null; const companyName = latestFiling?.company_name ?? watchlistItem?.company_name ?? ticker; const financials = redactedFilings .filter((entry) => entry.metrics && FINANCIAL_FORMS.has(entry.filing_type)) .map((entry) => ({ filingDate: entry.filing_date, filingType: entry.filing_type, revenue: entry.metrics?.revenue ?? null, netIncome: entry.metrics?.netIncome ?? null, totalAssets: entry.metrics?.totalAssets ?? null, cash: entry.metrics?.cash ?? null, debt: entry.metrics?.debt ?? null })); const aiReports = redactedFilings .filter((entry) => entry.analysis?.text || entry.analysis?.legacyInsights) .slice(0, 8) .map((entry) => ({ accessionNumber: entry.accession_number, filingDate: entry.filing_date, filingType: entry.filing_type, provider: entry.analysis?.provider ?? 'unknown', model: entry.analysis?.model ?? 'unknown', summary: entry.analysis?.text ?? entry.analysis?.legacyInsights ?? '' })); return Response.json({ analysis: { company: { ticker, companyName, sector: watchlistItem?.sector ?? null, cik: latestFiling?.cik ?? null }, quote: liveQuote, position: holding, priceHistory, financials, filings: redactedFilings.slice(0, 20), aiReports } }); }, { query: t.Object({ ticker: t.String({ minLength: 1 }) }) }) .get('/financials/company', async ({ query }) => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } if (!FINANCIALS_V2_ENABLED) { return jsonError('Financial statements v2 is disabled', 404); } const ticker = typeof query.ticker === 'string' ? query.ticker.trim().toUpperCase() : ''; if (!ticker) { return jsonError('ticker is required'); } const mode = asStatementMode(query.mode); const statement = asStatementKind(query.statement); const window = asHistoryWindow(query.window); const includeDimensions = asBoolean(query.includeDimensions, false); const cursor = typeof query.cursor === 'string' && query.cursor.trim().length > 0 ? query.cursor.trim() : null; const limit = Number.isFinite(Number(query.limit)) ? Number(query.limit) : undefined; let payload = await getCompanyFinancialStatements({ ticker, mode, statement, window, includeDimensions, cursor, limit, v2Enabled: FINANCIALS_V2_ENABLED, queuedSync: false }); let queuedSync = false; const shouldQueueSync = cursor === null && ( payload.dataSourceStatus.pendingFilings > 0 || payload.coverage.filings === 0 || (window === 'all' && payload.nextCursor !== null) ); if (shouldQueueSync) { try { await enqueueTask({ userId: session.user.id, taskType: 'sync_filings', payload: { ticker, limit: defaultFinancialSyncLimit(window) }, priority: 88 }); queuedSync = true; } catch (error) { console.error(`[financials-v2-sync] failed for ${ticker}:`, error); } } if (queuedSync) { payload = { ...payload, dataSourceStatus: { ...payload.dataSourceStatus, queuedSync: true } }; } return Response.json({ financials: payload }); }, { query: t.Object({ ticker: t.String({ minLength: 1 }), mode: t.Optional(t.Union([t.Literal('standardized'), t.Literal('filing_faithful')])), statement: t.Optional(t.Union([ t.Literal('income'), t.Literal('balance'), t.Literal('cash_flow'), t.Literal('equity'), t.Literal('comprehensive_income') ])), window: t.Optional(t.Union([t.Literal('10y'), t.Literal('all')])), includeDimensions: t.Optional(t.Union([t.String(), t.Boolean()])), cursor: t.Optional(t.String()), limit: t.Optional(t.Numeric()) }) }) .get('/analysis/reports/:accessionNumber', async ({ params }) => { const { response } = await requireAuthenticatedSession(); if (response) { return response; } const accessionNumber = params.accessionNumber?.trim() ?? ''; if (accessionNumber.length < 4) { return jsonError('Invalid accession number'); } const filing = await getFilingByAccession(accessionNumber); if (!filing) { return jsonError('AI summary not found', 404); } const summary = filing.analysis?.text ?? filing.analysis?.legacyInsights ?? ''; if (!summary) { return jsonError('AI summary not found', 404); } return Response.json({ report: { accessionNumber: filing.accession_number, ticker: filing.ticker, companyName: filing.company_name, filingDate: filing.filing_date, filingType: filing.filing_type, provider: filing.analysis?.provider ?? 'unknown', model: filing.analysis?.model ?? 'unknown', summary, filingUrl: filing.filing_url, submissionUrl: filing.submission_url ?? null, primaryDocument: filing.primary_document ?? null } }); }, { params: t.Object({ accessionNumber: t.String({ minLength: 4 }) }) }) .get('/filings', async ({ query }) => { const { response } = await requireAuthenticatedSession(); if (response) { return response; } const tickerFilter = typeof query.ticker === 'string' ? query.ticker.trim().toUpperCase() : undefined; const limit = typeof query.limit === 'number' ? query.limit : Number(query.limit); const filings = await listFilingsRecords({ ticker: tickerFilter, limit: Number.isFinite(limit) ? limit : 50 }); return Response.json({ filings: filings.map(redactInternalFilingAnalysisFields).map(withFinancialMetricsPolicy) }); }, { query: t.Object({ ticker: t.Optional(t.String()), limit: t.Optional(t.Numeric()) }) }) .post('/filings/sync', async ({ body }) => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const payload = asRecord(body); const ticker = typeof payload.ticker === 'string' ? payload.ticker.trim().toUpperCase() : ''; if (!ticker) { return jsonError('ticker is required'); } try { const limit = typeof payload.limit === 'number' ? payload.limit : Number(payload.limit); const task = await enqueueTask({ userId: session.user.id, taskType: 'sync_filings', payload: { ticker, limit: Number.isFinite(limit) ? limit : 20 }, priority: 90 }); return Response.json({ task }); } catch (error) { return jsonError(asErrorMessage(error, 'Failed to queue filings sync task')); } }, { body: t.Object({ ticker: t.String({ minLength: 1 }), limit: t.Optional(t.Numeric()) }) }) .post('/filings/:accessionNumber/analyze', async ({ params }) => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const accessionNumber = params.accessionNumber?.trim() ?? ''; if (accessionNumber.length < 4) { return jsonError('Invalid accession number'); } try { const task = await enqueueTask({ userId: session.user.id, taskType: 'analyze_filing', payload: { accessionNumber }, priority: 65 }); return Response.json({ task }); } catch (error) { return jsonError(asErrorMessage(error, 'Failed to queue filing analysis task')); } }, { params: t.Object({ accessionNumber: t.String({ minLength: 4 }) }) }) .get('/tasks', async ({ query }) => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const limit = typeof query.limit === 'number' ? query.limit : Number(query.limit ?? 20); const statusInput = query.status; const rawStatuses = Array.isArray(statusInput) ? statusInput : statusInput ? [statusInput] : []; const statuses = rawStatuses.filter((status): status is TaskStatus => { return ALLOWED_STATUSES.includes(status as TaskStatus); }); const tasks = await listRecentTasks( session.user.id, Number.isFinite(limit) ? limit : 20, statuses.length > 0 ? statuses : undefined ); return Response.json({ tasks }); }, { query: t.Object({ limit: t.Optional(t.Numeric()), status: t.Optional(t.Union([t.String(), t.Array(t.String())])) }) }) .get('/tasks/:taskId', async ({ params }) => { const { session, response } = await requireAuthenticatedSession(); if (response) { return response; } const task = await getTaskById(params.taskId, session.user.id); if (!task) { return jsonError('Task not found', 404); } return Response.json({ task }); }, { params: t.Object({ taskId: t.String({ minLength: 1 }) }) }); export type App = typeof app;