import type { CompanyFinancialStatementsResponse, FinancialCadence, FinancialDisplayMode, FinancialStatementKind, FinancialStatementPeriod, FinancialSurfaceKind, StandardizedFinancialRow, StructuredKpiRow, TaxonomyFactRow, TaxonomyStatementRow } from '@/lib/types'; import { listFilingsRecords } from '@/lib/server/repos/filings'; import { countFilingTaxonomySnapshotStatuses, listFilingTaxonomySnapshotsByTicker, listTaxonomyFactsByTicker, type FilingTaxonomySnapshotRecord } from '@/lib/server/repos/filing-taxonomy'; import { buildLtmFaithfulRows, buildLtmPeriods, buildRows, isStatementSurface, periodSorter, selectPrimaryPeriodsByCadence, surfaceToStatementKind } from '@/lib/server/financials/cadence'; import { readCachedFinancialBundle, writeFinancialBundle } from '@/lib/server/financials/bundles'; import { buildDimensionBreakdown, buildLtmStandardizedRows, buildStandardizedRows } from '@/lib/server/financials/standardize'; import { buildRatioRows } from '@/lib/server/financials/ratios'; import { buildFinancialCategories, buildTrendSeries } from '@/lib/server/financials/trend-series'; import { getHistoricalClosingPrices } from '@/lib/server/prices'; import { resolveKpiDefinitions } from '@/lib/server/financials/kpi-registry'; import { extractStructuredKpisFromDimensions } from '@/lib/server/financials/kpi-dimensions'; import { extractStructuredKpisFromNotes } from '@/lib/server/financials/kpi-notes'; type DimensionBreakdownMap = Record[string]>; type GetCompanyFinancialsInput = { ticker: string; surfaceKind: FinancialSurfaceKind; cadence: FinancialCadence; includeDimensions: boolean; includeFacts: boolean; factsCursor?: string | null; factsLimit?: number; cursor?: string | null; limit?: number; queuedSync: boolean; v3Enabled: boolean; }; type StandardizedStatementBundlePayload = { rows: StandardizedFinancialRow[]; trendSeries: CompanyFinancialStatementsResponse['trendSeries']; categories: CompanyFinancialStatementsResponse['categories']; }; type FilingDocumentRef = { filingId: number; cik: string; accessionNumber: string; filingUrl: string | null; primaryDocument: string | null; }; function safeTicker(input: string) { return input.trim().toUpperCase(); } function isFinancialForm(type: string): type is '10-K' | '10-Q' { return type === '10-K' || type === '10-Q'; } function cadenceFilingTypes(cadence: FinancialCadence) { return cadence === 'annual' ? ['10-K'] as Array<'10-K' | '10-Q'> : ['10-Q'] as Array<'10-K' | '10-Q'>; } function latestMetrics(snapshots: FilingTaxonomySnapshotRecord[]) { for (const snapshot of snapshots) { if (snapshot.derived_metrics) { return { taxonomy: snapshot.derived_metrics, validation: snapshot.validation_result }; } } return { taxonomy: null, validation: null }; } function defaultDisplayModes(surfaceKind: FinancialSurfaceKind): FinancialDisplayMode[] { return isStatementSurface(surfaceKind) ? ['standardized', 'faithful'] : ['standardized']; } function rekeyRowsByFilingId; resolvedSourceRowKeys?: Record; }>(rows: T[], sourcePeriods: FinancialStatementPeriod[], targetPeriods: FinancialStatementPeriod[]) { const targetPeriodByFilingId = new Map(targetPeriods.map((period) => [period.filingId, period])); return rows.map((row) => { const nextValues: Record = {}; const nextResolvedSourceRowKeys: Record = {}; for (const sourcePeriod of sourcePeriods) { const targetPeriod = targetPeriodByFilingId.get(sourcePeriod.filingId); if (!targetPeriod) { continue; } nextValues[targetPeriod.id] = row.values[sourcePeriod.id] ?? null; if (row.resolvedSourceRowKeys) { nextResolvedSourceRowKeys[targetPeriod.id] = row.resolvedSourceRowKeys[sourcePeriod.id] ?? null; } } return { ...row, values: nextValues, ...(row.resolvedSourceRowKeys ? { resolvedSourceRowKeys: nextResolvedSourceRowKeys } : {}) }; }); } function mergeDimensionBreakdownMaps(...maps: Array) { const merged = new Map[string]>(); for (const map of maps) { if (!map) { continue; } for (const [key, rows] of Object.entries(map)) { const existing = merged.get(key); if (existing) { existing.push(...rows); } else { merged.set(key, [...rows]); } } } return merged.size > 0 ? Object.fromEntries(merged.entries()) : null; } function buildKpiDimensionBreakdown(input: { rows: StructuredKpiRow[]; periods: FinancialStatementPeriod[]; facts: TaxonomyFactRow[]; }) { const map = new Map[string]>(); for (const row of input.rows) { if (row.provenanceType !== 'taxonomy') { continue; } const matchedFacts = input.facts.filter((fact) => row.sourceFactIds.includes(fact.id)); if (matchedFacts.length === 0) { continue; } map.set(row.key, matchedFacts.flatMap((fact) => { const matchedPeriod = input.periods.find((period) => period.filingId === fact.filingId); if (!matchedPeriod) { return []; } return fact.dimensions.map((dimension) => ({ rowKey: row.key, concept: fact.qname, sourceRowKey: fact.conceptKey, sourceLabel: row.label, periodId: matchedPeriod.id, axis: dimension.axis, member: dimension.member, value: fact.value, unit: fact.unit, provenanceType: 'taxonomy' as const })); })); } return map.size > 0 ? Object.fromEntries(map.entries()) : null; } function latestPeriodDate(period: FinancialStatementPeriod) { return period.periodEnd ?? period.filingDate; } function buildEmptyResponse(input: { ticker: string; companyName: string; cik: string | null; surfaceKind: FinancialSurfaceKind; cadence: FinancialCadence; queuedSync: boolean; enabled: boolean; metrics: CompanyFinancialStatementsResponse['metrics']; nextCursor: string | null; coverageFacts: number; }) { return { company: { ticker: input.ticker, companyName: input.companyName, cik: input.cik }, surfaceKind: input.surfaceKind, cadence: input.cadence, displayModes: defaultDisplayModes(input.surfaceKind), defaultDisplayMode: 'standardized', periods: [], statementRows: isStatementSurface(input.surfaceKind) ? { faithful: [], standardized: [] } : null, ratioRows: input.surfaceKind === 'ratios' ? [] : null, kpiRows: input.surfaceKind === 'segments_kpis' ? [] : null, trendSeries: [], categories: [], availability: { adjusted: false, customMetrics: false }, nextCursor: input.nextCursor, facts: null, coverage: { filings: 0, rows: 0, dimensions: 0, facts: input.coverageFacts }, dataSourceStatus: { enabled: input.enabled, hydratedFilings: 0, partialFilings: 0, failedFilings: 0, pendingFilings: 0, queuedSync: input.queuedSync }, metrics: input.metrics, dimensionBreakdown: null } satisfies CompanyFinancialStatementsResponse; } async function buildStatementSurfaceBundle(input: { surfaceKind: Extract; cadence: FinancialCadence; periods: FinancialStatementPeriod[]; faithfulRows: TaxonomyStatementRow[]; facts: TaxonomyFactRow[]; snapshots: FilingTaxonomySnapshotRecord[]; }) { const cached = await readCachedFinancialBundle({ ticker: input.snapshots[0]?.ticker ?? '', surfaceKind: input.surfaceKind, cadence: input.cadence, snapshots: input.snapshots }); if (cached) { return cached as StandardizedStatementBundlePayload; } const statement = surfaceToStatementKind(input.surfaceKind); if (!statement || (statement !== 'income' && statement !== 'balance' && statement !== 'cash_flow')) { return { rows: [], trendSeries: [], categories: [] } satisfies StandardizedStatementBundlePayload; } const standardizedRows = buildStandardizedRows({ rows: input.faithfulRows, statement, periods: input.periods, facts: input.facts }); const payload = { rows: standardizedRows, trendSeries: buildTrendSeries({ surfaceKind: input.surfaceKind, statementRows: standardizedRows }), categories: buildFinancialCategories(standardizedRows, input.surfaceKind) } satisfies StandardizedStatementBundlePayload; await writeFinancialBundle({ ticker: input.snapshots[0]?.ticker ?? '', surfaceKind: input.surfaceKind, cadence: input.cadence, snapshots: input.snapshots, payload: payload as unknown as Record }); return payload; } async function buildRatioSurfaceBundle(input: { ticker: string; cadence: FinancialCadence; periods: FinancialStatementPeriod[]; snapshots: FilingTaxonomySnapshotRecord[]; incomeRows: StandardizedFinancialRow[]; balanceRows: StandardizedFinancialRow[]; cashFlowRows: StandardizedFinancialRow[]; }) { const cached = await readCachedFinancialBundle({ ticker: input.ticker, surfaceKind: 'ratios', cadence: input.cadence, snapshots: input.snapshots }); if (cached) { return cached as Pick; } const pricesByDate = await getHistoricalClosingPrices(input.ticker, input.periods.map((period) => latestPeriodDate(period))); const pricesByPeriodId = Object.fromEntries(input.periods.map((period) => [period.id, pricesByDate[latestPeriodDate(period)] ?? null])); const ratioRows = buildRatioRows({ periods: input.periods, cadence: input.cadence, rows: { income: input.incomeRows, balance: input.balanceRows, cashFlow: input.cashFlowRows }, pricesByPeriodId }); const payload = { ratioRows, trendSeries: buildTrendSeries({ surfaceKind: 'ratios', ratioRows }), categories: buildFinancialCategories(ratioRows, 'ratios') } satisfies Pick; await writeFinancialBundle({ ticker: input.ticker, surfaceKind: 'ratios', cadence: input.cadence, snapshots: input.snapshots, payload: payload as unknown as Record }); return payload; } async function buildKpiSurfaceBundle(input: { ticker: string; cadence: FinancialCadence; periods: FinancialStatementPeriod[]; snapshots: FilingTaxonomySnapshotRecord[]; facts: TaxonomyFactRow[]; filings: FilingDocumentRef[]; }) { const cached = await readCachedFinancialBundle({ ticker: input.ticker, surfaceKind: 'segments_kpis', cadence: input.cadence, snapshots: input.snapshots }); if (cached) { return cached as Pick; } const resolved = resolveKpiDefinitions(input.ticker); if (!resolved.template) { return { kpiRows: [], trendSeries: [], categories: [] }; } const taxonomyRows = extractStructuredKpisFromDimensions({ facts: input.facts, periods: input.periods, definitions: resolved.definitions }); const noteRows = await extractStructuredKpisFromNotes({ ticker: input.ticker, periods: input.periods, filings: input.filings, definitions: resolved.definitions }); const rowsByKey = new Map(); for (const row of [...taxonomyRows, ...noteRows]) { const existing = rowsByKey.get(row.key); if (existing) { existing.values = { ...existing.values, ...row.values }; continue; } rowsByKey.set(row.key, row); } const kpiRows = [...rowsByKey.values()].sort((left, right) => { if (left.order !== right.order) { return left.order - right.order; } return left.label.localeCompare(right.label); }); const payload = { kpiRows, trendSeries: buildTrendSeries({ surfaceKind: 'segments_kpis', kpiRows }), categories: buildFinancialCategories(kpiRows, 'segments_kpis') } satisfies Pick; await writeFinancialBundle({ ticker: input.ticker, surfaceKind: 'segments_kpis', cadence: input.cadence, snapshots: input.snapshots, payload: payload as unknown as Record }); return payload; } export function defaultFinancialSyncLimit() { return 60; } export async function getCompanyFinancials(input: GetCompanyFinancialsInput): Promise { const ticker = safeTicker(input.ticker); const filingTypes = cadenceFilingTypes(input.cadence); const safeLimit = Math.min(Math.max(Math.trunc(input.limit ?? 12), 1), 40); const snapshotLimit = input.cadence === 'ltm' ? safeLimit + 3 : safeLimit; const [snapshotResult, statuses, filings] = await Promise.all([ listFilingTaxonomySnapshotsByTicker({ ticker, window: 'all', filingTypes: [...filingTypes], limit: snapshotLimit, cursor: input.cursor }), countFilingTaxonomySnapshotStatuses(ticker), listFilingsRecords({ ticker, limit: 250 }) ]); const latestFiling = filings[0] ?? null; const financialFilings = filings.filter((filing) => isFinancialForm(filing.filing_type)); const metrics = latestMetrics(snapshotResult.snapshots); if (snapshotResult.snapshots.length === 0) { return buildEmptyResponse({ ticker, companyName: latestFiling?.company_name ?? ticker, cik: latestFiling?.cik ?? null, surfaceKind: input.surfaceKind, cadence: input.cadence, queuedSync: input.queuedSync, enabled: input.v3Enabled, metrics, nextCursor: snapshotResult.nextCursor, coverageFacts: 0 }); } if (input.surfaceKind === 'adjusted' || input.surfaceKind === 'custom_metrics') { return { ...buildEmptyResponse({ ticker, companyName: latestFiling?.company_name ?? ticker, cik: latestFiling?.cik ?? null, surfaceKind: input.surfaceKind, cadence: input.cadence, queuedSync: input.queuedSync, enabled: input.v3Enabled, metrics, nextCursor: snapshotResult.nextCursor, coverageFacts: 0 }), dataSourceStatus: { enabled: input.v3Enabled, hydratedFilings: statuses.ready, partialFilings: statuses.partial, failedFilings: statuses.failed, pendingFilings: Math.max(0, financialFilings.filter((filing) => filingTypes.includes(filing.filing_type as '10-K' | '10-Q')).length - statuses.ready - statuses.partial - statuses.failed), queuedSync: input.queuedSync } }; } const allFacts = await listTaxonomyFactsByTicker({ ticker, window: 'all', filingTypes: [...filingTypes], limit: 10000 }); if (isStatementSurface(input.surfaceKind)) { const statement = surfaceToStatementKind(input.surfaceKind); if (!statement) { throw new Error(`Unsupported statement surface ${input.surfaceKind}`); } const selection = input.cadence === 'ltm' ? selectPrimaryPeriodsByCadence(snapshotResult.snapshots, statement, 'quarterly') : selectPrimaryPeriodsByCadence(snapshotResult.snapshots, statement, input.cadence); const periods = input.cadence === 'ltm' ? buildLtmPeriods(selection.periods) : selection.periods; const faithfulRows = input.cadence === 'ltm' ? buildLtmFaithfulRows( buildRows(selection.snapshots, statement, selection.selectedPeriodIds), selection.periods, periods, statement ) : buildRows(selection.snapshots, statement, selection.selectedPeriodIds); const factsForStatement = allFacts.facts.filter((fact) => fact.statement === statement); const factsForStandardization = allFacts.facts; const standardizedPayload = await buildStatementSurfaceBundle({ surfaceKind: input.surfaceKind as Extract, cadence: input.cadence, periods, faithfulRows, facts: factsForStandardization, snapshots: selection.snapshots }); const standardizedRows = input.cadence === 'ltm' ? buildLtmStandardizedRows( buildStandardizedRows({ rows: buildRows(selection.snapshots, statement, selection.selectedPeriodIds), statement: statement as Extract, periods: selection.periods, facts: factsForStandardization }), selection.periods, periods, statement as Extract ) : standardizedPayload.rows; const rawFacts = input.includeFacts ? await listTaxonomyFactsByTicker({ ticker, window: 'all', filingTypes: [...filingTypes], statement, cursor: input.factsCursor, limit: input.factsLimit }) : { facts: [], nextCursor: null }; const dimensionBreakdown = input.includeDimensions ? buildDimensionBreakdown(factsForStandardization, periods, faithfulRows, standardizedRows) : null; return { company: { ticker, companyName: latestFiling?.company_name ?? ticker, cik: latestFiling?.cik ?? null }, surfaceKind: input.surfaceKind, cadence: input.cadence, displayModes: defaultDisplayModes(input.surfaceKind), defaultDisplayMode: 'standardized', periods, statementRows: { faithful: faithfulRows, standardized: standardizedRows }, ratioRows: null, kpiRows: null, trendSeries: buildTrendSeries({ surfaceKind: input.surfaceKind, statementRows: standardizedRows }), categories: standardizedPayload.categories, availability: { adjusted: false, customMetrics: false }, nextCursor: snapshotResult.nextCursor, facts: input.includeFacts ? { rows: rawFacts.facts, nextCursor: rawFacts.nextCursor } : null, coverage: { filings: periods.length, rows: standardizedRows.length, dimensions: dimensionBreakdown ? Object.values(dimensionBreakdown).reduce((sum, rows) => sum + rows.length, 0) : 0, facts: input.includeFacts ? rawFacts.facts.length : allFacts.facts.length }, dataSourceStatus: { enabled: input.v3Enabled, hydratedFilings: statuses.ready, partialFilings: statuses.partial, failedFilings: statuses.failed, pendingFilings: Math.max(0, financialFilings.filter((filing) => filingTypes.includes(filing.filing_type as '10-K' | '10-Q')).length - statuses.ready - statuses.partial - statuses.failed), queuedSync: input.queuedSync }, metrics, dimensionBreakdown }; } const incomeSelection = input.cadence === 'ltm' ? selectPrimaryPeriodsByCadence(snapshotResult.snapshots, 'income', 'quarterly') : selectPrimaryPeriodsByCadence(snapshotResult.snapshots, 'income', input.cadence); const balanceSelection = input.cadence === 'ltm' ? selectPrimaryPeriodsByCadence(snapshotResult.snapshots, 'balance', 'quarterly') : selectPrimaryPeriodsByCadence(snapshotResult.snapshots, 'balance', input.cadence); const cashFlowSelection = input.cadence === 'ltm' ? selectPrimaryPeriodsByCadence(snapshotResult.snapshots, 'cash_flow', 'quarterly') : selectPrimaryPeriodsByCadence(snapshotResult.snapshots, 'cash_flow', input.cadence); const basePeriods = input.cadence === 'ltm' ? buildLtmPeriods(incomeSelection.periods) : incomeSelection.periods; const incomeQuarterlyRows = buildStandardizedRows({ rows: buildRows(incomeSelection.snapshots, 'income', incomeSelection.selectedPeriodIds), statement: 'income', periods: incomeSelection.periods, facts: allFacts.facts }); const balanceQuarterlyRows = rekeyRowsByFilingId(buildStandardizedRows({ rows: buildRows(balanceSelection.snapshots, 'balance', balanceSelection.selectedPeriodIds), statement: 'balance', periods: balanceSelection.periods, facts: allFacts.facts }), balanceSelection.periods, incomeSelection.periods); const cashFlowQuarterlyRows = rekeyRowsByFilingId(buildStandardizedRows({ rows: buildRows(cashFlowSelection.snapshots, 'cash_flow', cashFlowSelection.selectedPeriodIds), statement: 'cash_flow', periods: cashFlowSelection.periods, facts: allFacts.facts }), cashFlowSelection.periods, incomeSelection.periods); const incomeRows = input.cadence === 'ltm' ? buildLtmStandardizedRows(incomeQuarterlyRows, incomeSelection.periods, basePeriods, 'income') : incomeQuarterlyRows; const balanceRows = input.cadence === 'ltm' ? buildLtmStandardizedRows(balanceQuarterlyRows, incomeSelection.periods, basePeriods, 'balance') : balanceQuarterlyRows; const cashFlowRows = input.cadence === 'ltm' ? buildLtmStandardizedRows(cashFlowQuarterlyRows, incomeSelection.periods, basePeriods, 'cash_flow') : cashFlowQuarterlyRows; if (input.surfaceKind === 'ratios') { const ratioBundle = await buildRatioSurfaceBundle({ ticker, cadence: input.cadence, periods: basePeriods, snapshots: incomeSelection.snapshots, incomeRows, balanceRows, cashFlowRows }); return { company: { ticker, companyName: latestFiling?.company_name ?? ticker, cik: latestFiling?.cik ?? null }, surfaceKind: input.surfaceKind, cadence: input.cadence, displayModes: defaultDisplayModes(input.surfaceKind), defaultDisplayMode: 'standardized', periods: basePeriods, statementRows: null, ratioRows: ratioBundle.ratioRows, kpiRows: null, trendSeries: ratioBundle.trendSeries, categories: ratioBundle.categories, availability: { adjusted: false, customMetrics: false }, nextCursor: snapshotResult.nextCursor, facts: null, coverage: { filings: basePeriods.length, rows: ratioBundle.ratioRows?.length ?? 0, dimensions: 0, facts: allFacts.facts.length }, dataSourceStatus: { enabled: input.v3Enabled, hydratedFilings: statuses.ready, partialFilings: statuses.partial, failedFilings: statuses.failed, pendingFilings: Math.max(0, financialFilings.filter((filing) => filingTypes.includes(filing.filing_type as '10-K' | '10-Q')).length - statuses.ready - statuses.partial - statuses.failed), queuedSync: input.queuedSync }, metrics, dimensionBreakdown: null }; } const filingRefs: FilingDocumentRef[] = filings.map((filing) => ({ filingId: filing.id, cik: filing.cik, accessionNumber: filing.accession_number, filingUrl: filing.filing_url ?? null, primaryDocument: filing.primary_document ?? null })); const kpiBundle = await buildKpiSurfaceBundle({ ticker, cadence: input.cadence, periods: basePeriods, snapshots: incomeSelection.snapshots, facts: allFacts.facts, filings: filingRefs }); const kpiBreakdown = input.includeDimensions ? buildKpiDimensionBreakdown({ rows: kpiBundle.kpiRows ?? [], periods: basePeriods, facts: allFacts.facts }) : null; return { company: { ticker, companyName: latestFiling?.company_name ?? ticker, cik: latestFiling?.cik ?? null }, surfaceKind: input.surfaceKind, cadence: input.cadence, displayModes: defaultDisplayModes(input.surfaceKind), defaultDisplayMode: 'standardized', periods: basePeriods, statementRows: null, ratioRows: null, kpiRows: kpiBundle.kpiRows, trendSeries: kpiBundle.trendSeries, categories: kpiBundle.categories, availability: { adjusted: false, customMetrics: false }, nextCursor: snapshotResult.nextCursor, facts: null, coverage: { filings: basePeriods.length, rows: kpiBundle.kpiRows?.length ?? 0, dimensions: kpiBreakdown ? Object.values(kpiBreakdown).reduce((sum, rows) => sum + rows.length, 0) : 0, facts: allFacts.facts.length }, dataSourceStatus: { enabled: input.v3Enabled, hydratedFilings: statuses.ready, partialFilings: statuses.partial, failedFilings: statuses.failed, pendingFilings: Math.max(0, financialFilings.filter((filing) => filingTypes.includes(filing.filing_type as '10-K' | '10-Q')).length - statuses.ready - statuses.partial - statuses.failed), queuedSync: input.queuedSync }, metrics, dimensionBreakdown: mergeDimensionBreakdownMaps(kpiBreakdown) }; } export async function getCompanyFinancialTaxonomy(input: GetCompanyFinancialsInput) { return await getCompanyFinancials(input); } export const __financialTaxonomyInternals = { buildRows, buildStandardizedRows, buildDimensionBreakdown, periodSorter, selectPrimaryPeriodsByCadence, buildLtmPeriods, buildLtmFaithfulRows, buildLtmStandardizedRows };