Files
Neon-Desk/lib/server/financials/kpi-dimensions.ts
francy51 db01f207a5 Expand financials surfaces with ratios, KPIs, and cadence support
- Add bundled financial modeling pipeline (ratios, KPI dimensions/notes, trend series, standardization)
- Introduce company financial bundles storage (Drizzle migration + repo wiring)
- Refactor financials page/API/query flow to use surfaceKind + cadence and new response shapes
2026-03-07 15:16:35 -05:00

160 lines
5.3 KiB
TypeScript

import type {
FinancialStatementPeriod,
StructuredKpiRow,
TaxonomyFactRow
} from '@/lib/types';
import type { KpiDefinition } from '@/lib/server/financials/kpi-registry';
import { factMatchesPeriod } from '@/lib/server/financials/standardize';
function normalizeSegmentToken(value: string) {
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
}
function humanizeMember(value: string) {
const source = value.split(':').pop() ?? value;
return source
.replace(/Member$/i, '')
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/_/g, ' ')
.trim();
}
function factMatchesDefinition(fact: TaxonomyFactRow, definition: KpiDefinition) {
if (definition.preferredConceptNames && !definition.preferredConceptNames.includes(fact.localName)) {
return false;
}
if (!definition.preferredAxisIncludes || definition.preferredAxisIncludes.length === 0) {
return fact.dimensions.length > 0;
}
return fact.dimensions.some((dimension) => {
const axisMatch = definition.preferredAxisIncludes?.some((token) => dimension.axis.toLowerCase().includes(token.toLowerCase())) ?? false;
const memberMatch = definition.preferredMemberIncludes && definition.preferredMemberIncludes.length > 0
? definition.preferredMemberIncludes.some((token) => dimension.member.toLowerCase().includes(token.toLowerCase()))
: true;
return axisMatch && memberMatch;
});
}
function categoryForDefinition(definition: KpiDefinition, axis: string) {
if (definition.key === 'segment_revenue' && /geo|country|region|area/i.test(axis)) {
return 'geographic_mix';
}
return definition.category;
}
export function extractStructuredKpisFromDimensions(input: {
facts: TaxonomyFactRow[];
periods: FinancialStatementPeriod[];
definitions: KpiDefinition[];
}) {
const rowMap = new Map<string, StructuredKpiRow>();
const orderByKey = new Map<string, number>();
input.definitions.forEach((definition, index) => {
orderByKey.set(definition.key, (index + 1) * 10);
});
for (const definition of input.definitions) {
for (const fact of input.facts) {
if (fact.dimensions.length === 0 || !factMatchesDefinition(fact, definition)) {
continue;
}
const matchedPeriod = input.periods.find((period) => period.filingId === fact.filingId && factMatchesPeriod(fact, period));
if (!matchedPeriod) {
continue;
}
for (const dimension of fact.dimensions) {
const axis = dimension.axis;
const member = dimension.member;
const normalizedAxis = normalizeSegmentToken(axis);
const normalizedMember = normalizeSegmentToken(member);
const key = `${definition.key}__${normalizedAxis}__${normalizedMember}`;
const labelSuffix = humanizeMember(member);
const existing = rowMap.get(key);
if (existing) {
existing.values[matchedPeriod.id] = fact.value;
if (!existing.sourceConcepts.includes(fact.qname)) {
existing.sourceConcepts.push(fact.qname);
}
if (!existing.sourceFactIds.includes(fact.id)) {
existing.sourceFactIds.push(fact.id);
}
continue;
}
rowMap.set(key, {
key,
label: `${definition.label} - ${labelSuffix}`,
category: categoryForDefinition(definition, axis),
unit: definition.unit,
order: orderByKey.get(definition.key) ?? 999,
segment: labelSuffix || null,
axis,
member,
values: { [matchedPeriod.id]: fact.value },
sourceConcepts: [fact.qname],
sourceFactIds: [fact.id],
provenanceType: 'taxonomy',
hasDimensions: true
});
}
}
}
const rows = [...rowMap.values()].sort((left, right) => {
if (left.order !== right.order) {
return left.order - right.order;
}
return left.label.localeCompare(right.label);
});
const marginRows = new Map<string, StructuredKpiRow>();
for (const row of rows.filter((entry) => entry.category === 'segment_profit')) {
const revenueKey = row.key.replace(/^segment_profit__/, 'segment_revenue__');
const revenueRow = rowMap.get(revenueKey);
if (!revenueRow) {
continue;
}
const values: Record<string, number | null> = {};
for (const period of input.periods) {
const revenue = revenueRow.values[period.id] ?? null;
const profit = row.values[period.id] ?? null;
values[period.id] = revenue === null || profit === null || revenue === 0
? null
: profit / revenue;
}
marginRows.set(row.key.replace(/^segment_profit__/, 'segment_margin__'), {
key: row.key.replace(/^segment_profit__/, 'segment_margin__'),
label: row.label.replace(/^Segment Profit/, 'Segment Margin'),
category: 'segment_margin',
unit: 'percent',
order: 25,
segment: row.segment,
axis: row.axis,
member: row.member,
values,
sourceConcepts: [...new Set([...row.sourceConcepts, ...revenueRow.sourceConcepts])],
sourceFactIds: [...new Set([...row.sourceFactIds, ...revenueRow.sourceFactIds])],
provenanceType: 'taxonomy',
hasDimensions: true
});
}
return [...rows, ...marginRows.values()].sort((left, right) => {
if (left.order !== right.order) {
return left.order - right.order;
}
return left.label.localeCompare(right.label);
});
}