import { edenTreaty } from '@elysiajs/eden'; import type { App } from '@/lib/server/api/app'; import type { CompanyAiReportDetail, CompanyAnalysis, CompanyFinancialStatementsResponse, Filing, Holding, FinancialHistoryWindow, FinancialStatementKind, PortfolioInsight, PortfolioSummary, Task, TaskStatus, TaskTimeline, User, WatchlistItem } from './types'; import { resolveApiBaseURL } from './runtime-url'; const API_BASE = resolveApiBaseURL(process.env.NEXT_PUBLIC_API_URL); const client = edenTreaty(API_BASE, { $fetch: { credentials: 'include', cache: 'no-store' } }); export class ApiError extends Error { status: number; constructor(message: string, status: number) { super(message); this.name = 'ApiError'; this.status = status; } } function extractErrorMessage(error: unknown, fallback: string) { if (!error || typeof error !== 'object') { return fallback; } const candidate = error as { value?: unknown; message?: string; }; if (typeof candidate.message === 'string' && candidate.message.trim().length > 0) { return candidate.message; } if (candidate.value && typeof candidate.value === 'object') { const nested = candidate.value as { error?: unknown; message?: unknown }; if (typeof nested.error === 'string' && nested.error.trim().length > 0) { return nested.error; } if (typeof nested.message === 'string' && nested.message.trim().length > 0) { return nested.message; } } if (typeof candidate.value === 'string' && candidate.value.trim().length > 0) { return candidate.value; } return fallback; } type TreatyResult = { data: unknown; error: unknown; status: number; }; async function unwrapData(result: TreatyResult, fallback: string) { if (result.error) { throw new ApiError( extractErrorMessage(result.error, fallback), result.status ); } if (result.data === null || result.data === undefined) { throw new ApiError(fallback, result.status); } const payload = result.data instanceof Response ? await result.data.json().catch(() => null) : result.data; if (payload === null || payload === undefined) { throw new ApiError(fallback, result.status); } return payload as T; } export async function getMe() { const result = await client.api.me.get(); return await unwrapData<{ user: User }>(result, 'Unable to fetch session'); } export async function listWatchlist() { const result = await client.api.watchlist.get(); return await unwrapData<{ items: WatchlistItem[] }>(result, 'Unable to fetch watchlist'); } export async function upsertWatchlistItem(input: { ticker: string; companyName: string; sector?: string; category?: string; tags?: string[]; }) { const result = await client.api.watchlist.post(input); return await unwrapData<{ item: WatchlistItem }>(result, 'Unable to save watchlist item'); } export async function deleteWatchlistItem(id: number) { const result = await client.api.watchlist[id].delete(); return await unwrapData<{ success: boolean }>(result, 'Unable to delete watchlist item'); } export async function listHoldings() { const result = await client.api.portfolio.holdings.get(); return await unwrapData<{ holdings: Holding[] }>(result, 'Unable to fetch holdings'); } export async function getPortfolioSummary() { const result = await client.api.portfolio.summary.get(); return await unwrapData<{ summary: PortfolioSummary }>(result, 'Unable to fetch summary'); } export async function upsertHolding(input: { ticker: string; shares: number; avgCost: number; currentPrice?: number; }) { const result = await client.api.portfolio.holdings.post(input); return await unwrapData<{ holding: Holding }>(result, 'Unable to save holding'); } export async function deleteHolding(id: number) { const result = await client.api.portfolio.holdings[id].delete(); return await unwrapData<{ success: boolean }>(result, 'Unable to delete holding'); } export async function queuePriceRefresh() { const result = await client.api.portfolio['refresh-prices'].post(); return await unwrapData<{ task: Task }>(result, 'Unable to queue price refresh'); } export async function queuePortfolioInsights() { const result = await client.api.portfolio.insights.generate.post(); return await unwrapData<{ task: Task }>(result, 'Unable to queue portfolio insights'); } export async function getLatestPortfolioInsight() { const result = await client.api.portfolio.insights.latest.get(); return await unwrapData<{ insight: PortfolioInsight | null }>(result, 'Unable to fetch latest insight'); } export async function listFilings(query?: { ticker?: string; limit?: number }) { const queryParams: { ticker?: string; limit?: number; } = {}; if (query?.ticker?.trim()) { queryParams.ticker = query.ticker.trim().toUpperCase(); } if (query?.limit !== undefined) { queryParams.limit = query.limit; } const result = await client.api.filings.get({ $query: queryParams }); return await unwrapData<{ filings: Filing[] }>(result, 'Unable to fetch filings'); } export async function getCompanyAnalysis(ticker: string) { const result = await client.api.analysis.company.get({ $query: { ticker: ticker.trim().toUpperCase() } }); return await unwrapData<{ analysis: CompanyAnalysis }>(result, 'Unable to fetch company analysis'); } export async function getCompanyFinancialStatements(input: { ticker: string; statement: FinancialStatementKind; window: FinancialHistoryWindow; includeDimensions?: boolean; includeFacts?: boolean; factsCursor?: string | null; factsLimit?: number; cursor?: string | null; limit?: number; }) { const query = { ticker: input.ticker.trim().toUpperCase(), statement: input.statement, window: input.window, includeDimensions: input.includeDimensions ? 'true' : 'false', includeFacts: input.includeFacts ? 'true' : 'false', ...(typeof input.cursor === 'string' && input.cursor.trim().length > 0 ? { cursor: input.cursor.trim() } : {}), ...(typeof input.limit === 'number' && Number.isFinite(input.limit) ? { limit: input.limit } : {}), ...(typeof input.factsCursor === 'string' && input.factsCursor.trim().length > 0 ? { factsCursor: input.factsCursor.trim() } : {}), ...(typeof input.factsLimit === 'number' && Number.isFinite(input.factsLimit) ? { factsLimit: input.factsLimit } : {}) }; const result = await client.api.financials.company.get({ $query: query }); return await unwrapData<{ financials: CompanyFinancialStatementsResponse }>( result, 'Unable to fetch company financial statements' ); } export async function getCompanyAiReport(accessionNumber: string) { const normalizedAccession = accessionNumber.trim(); const result = await client.api.analysis.reports[normalizedAccession].get(); return await unwrapData<{ report: CompanyAiReportDetail }>(result, 'Unable to fetch AI summary'); } export async function queueFilingSync(input: { ticker: string; limit?: number; category?: string; tags?: string[]; }) { const result = await client.api.filings.sync.post(input); return await unwrapData<{ task: Task }>(result, 'Unable to queue filing sync'); } export async function queueFilingAnalysis(accessionNumber: string) { const result = await client.api.filings[accessionNumber].analyze.post(); return await unwrapData<{ task: Task }>(result, 'Unable to queue filing analysis'); } export async function getTask(taskId: string) { const result = await client.api.tasks[taskId].get(); return await unwrapData<{ task: Task }>(result, 'Unable to fetch task'); } export async function getTaskTimeline(taskId: string) { const result = await client.api.tasks[taskId].timeline.get(); return await unwrapData(result, 'Unable to fetch task timeline'); } export async function updateTaskNotificationState( taskId: string, input: { read?: boolean; silenced?: boolean } ) { const result = await client.api.tasks[taskId].notification.patch(input); return await unwrapData<{ task: Task }>(result, 'Unable to update task notification state'); } export async function listRecentTasks(input: { limit?: number; statuses?: TaskStatus[]; } = {}) { const result = await client.api.tasks.get({ $query: { limit: input.limit ?? 20, ...(input.statuses && input.statuses.length > 0 ? { status: input.statuses } : {}) } }); return await unwrapData<{ tasks: Task[] }>(result, 'Unable to fetch tasks'); }