Files
Neon-Desk/lib/server/financials/trend-series.ts

89 lines
2.8 KiB
TypeScript

import type {
FinancialSurfaceKind,
RatioRow,
StandardizedFinancialRow,
StructuredKpiRow,
TrendSeries
} from '@/lib/types';
import { RATIO_CATEGORIES } from '@/lib/generated';
import { KPI_CATEGORY_ORDER } from '@/lib/server/financials/kpi-registry';
function toTrendSeriesRow(row: {
key: string;
label: string;
category: string;
unit: TrendSeries['unit'];
values: Record<string, number | null>;
}) {
return {
key: row.key,
label: row.label,
category: row.category,
unit: row.unit,
values: row.values
} satisfies TrendSeries;
}
export function buildFinancialCategories(rows: Array<{ category: string }>, surfaceKind: FinancialSurfaceKind) {
const counts = new Map<string, number>();
for (const row of rows) {
counts.set(row.category, (counts.get(row.category) ?? 0) + 1);
}
const order = surfaceKind === 'ratios'
? [...RATIO_CATEGORIES]
: surfaceKind === 'segments_kpis'
? [...KPI_CATEGORY_ORDER]
: [...counts.keys()];
return order
.filter((key) => (counts.get(key) ?? 0) > 0)
.map((key) => ({
key,
label: key.replace(/_/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()),
count: counts.get(key) ?? 0
}));
}
export function buildTrendSeries(input: {
surfaceKind: FinancialSurfaceKind;
statementRows?: StandardizedFinancialRow[];
ratioRows?: RatioRow[];
kpiRows?: StructuredKpiRow[];
}) {
switch (input.surfaceKind) {
case 'income_statement':
return (input.statementRows ?? [])
.filter((row) => row.key === 'revenue' || row.key === 'net_income')
.map(toTrendSeriesRow);
case 'balance_sheet':
return (input.statementRows ?? [])
.filter((row) => row.key === 'total_assets' || row.key === 'cash_and_equivalents' || row.key === 'total_debt')
.map(toTrendSeriesRow);
case 'cash_flow_statement':
return (input.statementRows ?? [])
.filter((row) => row.key === 'operating_cash_flow' || row.key === 'free_cash_flow' || row.key === 'capital_expenditures')
.map(toTrendSeriesRow);
case 'equity_statement':
return (input.statementRows ?? [])
.filter((row) => row.key === 'total_equity' || row.key === 'retained_earnings' || row.key === 'common_stock_and_apic')
.map(toTrendSeriesRow);
case 'disclosures':
return [];
case 'ratios':
return (input.ratioRows ?? [])
.filter((row) => row.category === 'margins')
.map(toTrendSeriesRow);
case 'segments_kpis': {
const rows = input.kpiRows ?? [];
const firstCategory = buildFinancialCategories(rows, 'segments_kpis')[0]?.key ?? null;
return rows
.filter((row) => row.category === firstCategory)
.slice(0, 4)
.map(toTrendSeriesRow);
}
default:
return [];
}
}