import type { CompanyFinancialStatementsResponse, FinancialUnit, RatioRow, StandardizedFinancialRow } from '@/lib/types'; import type { GraphableFinancialSurfaceKind } from '@/lib/financial-metrics'; type GraphingMetricRow = StandardizedFinancialRow | RatioRow; export type GraphingFetchResult = { ticker: string; financials?: CompanyFinancialStatementsResponse; error?: string; }; export type GraphingSeriesPoint = { periodId: string; dateKey: string; filingType: '10-K' | '10-Q'; filingDate: string; periodEnd: string | null; periodLabel: string; value: number | null; }; export type GraphingCompanySeries = { ticker: string; companyName: string; fiscalPack: string | null; status: 'ready' | 'error' | 'no_metric_data' | 'not_meaningful'; errorMessage: string | null; unit: FinancialUnit | null; points: GraphingSeriesPoint[]; latestPoint: GraphingSeriesPoint | null; priorPoint: GraphingSeriesPoint | null; }; export type GraphingLatestValueRow = { ticker: string; companyName: string; fiscalPack: string | null; status: GraphingCompanySeries['status']; errorMessage: string | null; latestValue: number | null; priorValue: number | null; changeValue: number | null; latestDateKey: string | null; latestPeriodLabel: string | null; latestFilingType: '10-K' | '10-Q' | null; }; export type GraphingChartDatum = Record & { dateKey: string; dateMs: number; }; type GraphingComparisonData = { companies: GraphingCompanySeries[]; chartData: GraphingChartDatum[]; latestRows: GraphingLatestValueRow[]; hasAnyData: boolean; hasPartialData: boolean; }; function sortPeriods(left: { periodEnd: string | null; filingDate: string }, right: { periodEnd: string | null; filingDate: string }) { return Date.parse(left.periodEnd ?? left.filingDate) - Date.parse(right.periodEnd ?? right.filingDate); } function extractMetricRow( financials: CompanyFinancialStatementsResponse, surface: GraphableFinancialSurfaceKind, metric: string ): GraphingMetricRow | null { if (surface === 'ratios') { return financials.ratioRows?.find((row) => row.key === metric) ?? null; } return financials.statementRows?.standardized.find((row) => row.key === metric) ?? null; } function extractCompanySeries( result: GraphingFetchResult, surface: GraphableFinancialSurfaceKind, metric: string ): GraphingCompanySeries { if (result.error || !result.financials) { return { ticker: result.ticker, companyName: result.ticker, fiscalPack: null, status: 'error', errorMessage: result.error ?? 'Unable to load financial history', unit: null, points: [], latestPoint: null, priorPoint: null }; } const metricRow = extractMetricRow(result.financials, surface, metric); const periods = [...result.financials.periods].sort(sortPeriods); const notMeaningful = surface !== 'ratios' && metricRow && 'resolutionMethod' in metricRow && metricRow.resolutionMethod === 'not_meaningful'; const points = periods.map((period) => ({ periodId: period.id, dateKey: period.periodEnd ?? period.filingDate, filingType: period.filingType, filingDate: period.filingDate, periodEnd: period.periodEnd, periodLabel: period.periodLabel, value: metricRow?.values[period.id] ?? null })); const populatedPoints = points.filter((point) => point.value !== null); const latestPoint = populatedPoints[populatedPoints.length - 1] ?? null; const priorPoint = populatedPoints.length > 1 ? populatedPoints[populatedPoints.length - 2] ?? null : null; return { ticker: result.financials.company.ticker, companyName: result.financials.company.companyName, fiscalPack: result.financials.normalization.fiscalPack, status: notMeaningful ? 'not_meaningful' : latestPoint ? 'ready' : 'no_metric_data', errorMessage: notMeaningful ? 'Not meaningful for this pack.' : latestPoint ? null : 'No data available for the selected metric.', unit: metricRow?.unit ?? null, points, latestPoint, priorPoint }; } export function buildGraphingComparisonData(input: { results: GraphingFetchResult[]; surface: GraphableFinancialSurfaceKind; metric: string; }): GraphingComparisonData { const companies = input.results.map((result) => extractCompanySeries(result, input.surface, input.metric)); const chartDatumByDate = new Map(); for (const company of companies) { for (const point of company.points) { const dateMs = Date.parse(point.dateKey); const existing = chartDatumByDate.get(point.dateKey) ?? { dateKey: point.dateKey, dateMs }; existing[company.ticker] = point.value; existing[`meta__${company.ticker}`] = point; chartDatumByDate.set(point.dateKey, existing); } } const chartData = [...chartDatumByDate.values()] .sort((left, right) => left.dateMs - right.dateMs) .map((datum) => { for (const company of companies) { if (!(company.ticker in datum)) { datum[company.ticker] = null; } } return datum; }); const latestRows = companies.map((company) => ({ ticker: company.ticker, companyName: company.companyName, fiscalPack: company.fiscalPack, status: company.status, errorMessage: company.errorMessage, latestValue: company.latestPoint?.value ?? null, priorValue: company.priorPoint?.value ?? null, changeValue: company.latestPoint?.value !== null && company.latestPoint?.value !== undefined && company.priorPoint?.value !== null && company.priorPoint?.value !== undefined ? company.latestPoint.value - company.priorPoint.value : null, latestDateKey: company.latestPoint?.dateKey ?? null, latestPeriodLabel: company.latestPoint?.periodLabel ?? null, latestFilingType: company.latestPoint?.filingType ?? null })); const hasAnyData = companies.some((company) => company.latestPoint !== null); const hasPartialData = companies.some((company) => company.status !== 'ready') || companies.some((company) => company.points.some((point) => point.value === null)); return { companies, chartData, latestRows, hasAnyData, hasPartialData }; }