1276 lines
41 KiB
TypeScript
1276 lines
41 KiB
TypeScript
import type {
|
|
CompanyFinancialStatementsResponse,
|
|
DetailFinancialRow,
|
|
FinancialCadence,
|
|
FinancialDisplayMode,
|
|
FinancialStatementKind,
|
|
FinancialStatementPeriod,
|
|
FinancialSurfaceKind,
|
|
NormalizationMetadata,
|
|
StandardizedFinancialRow,
|
|
StructuredKpiRow,
|
|
SurfaceDetailMap,
|
|
SurfaceFinancialRow,
|
|
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, NonNullable<CompanyFinancialStatementsResponse['dimensionBreakdown']>[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: SurfaceFinancialRow[];
|
|
detailRows: SurfaceDetailMap;
|
|
trendSeries: CompanyFinancialStatementsResponse['trendSeries'];
|
|
categories: CompanyFinancialStatementsResponse['categories'];
|
|
normalization: NormalizationMetadata;
|
|
};
|
|
|
|
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<T extends {
|
|
values: Record<string, number | null>;
|
|
resolvedSourceRowKeys?: Record<string, string | null>;
|
|
}>(rows: T[], sourcePeriods: FinancialStatementPeriod[], targetPeriods: FinancialStatementPeriod[]) {
|
|
const targetPeriodByFilingId = new Map(targetPeriods.map((period) => [period.filingId, period]));
|
|
|
|
return rows.map((row) => {
|
|
const nextValues: Record<string, number | null> = {};
|
|
const nextResolvedSourceRowKeys: Record<string, string | null> = {};
|
|
|
|
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<DimensionBreakdownMap | null>) {
|
|
const merged = new Map<string, NonNullable<CompanyFinancialStatementsResponse['dimensionBreakdown']>[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, NonNullable<CompanyFinancialStatementsResponse['dimensionBreakdown']>[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 cloneStructuredKpiRow(row: StructuredKpiRow): StructuredKpiRow {
|
|
return {
|
|
...row,
|
|
values: { ...(row.values ?? {}) },
|
|
sourceConcepts: [...(row.sourceConcepts ?? [])],
|
|
sourceFactIds: [...(row.sourceFactIds ?? [])]
|
|
};
|
|
}
|
|
|
|
function mergeStructuredKpiRowsByPriority(groups: StructuredKpiRow[][]) {
|
|
const rowsByKey = new Map<string, StructuredKpiRow>();
|
|
|
|
for (const group of groups) {
|
|
for (const row of group) {
|
|
const existing = rowsByKey.get(row.key);
|
|
if (!existing) {
|
|
rowsByKey.set(row.key, cloneStructuredKpiRow(row));
|
|
continue;
|
|
}
|
|
|
|
for (const [periodId, value] of Object.entries(row.values ?? {})) {
|
|
const hasExistingValue = Object.prototype.hasOwnProperty.call(existing.values, periodId)
|
|
&& existing.values[periodId] !== null;
|
|
if (!hasExistingValue) {
|
|
existing.values[periodId] = value;
|
|
}
|
|
}
|
|
|
|
existing.sourceConcepts = [...new Set([...existing.sourceConcepts, ...(row.sourceConcepts ?? [])])]
|
|
.sort((left, right) => left.localeCompare(right));
|
|
existing.sourceFactIds = [...new Set([...existing.sourceFactIds, ...(row.sourceFactIds ?? [])])]
|
|
.sort((left, right) => left - right);
|
|
existing.hasDimensions = existing.hasDimensions || row.hasDimensions;
|
|
existing.segment ??= row.segment;
|
|
existing.axis ??= row.axis;
|
|
existing.member ??= row.member;
|
|
}
|
|
}
|
|
|
|
return [...rowsByKey.values()].sort((left, right) => {
|
|
if (left.order !== right.order) {
|
|
return left.order - right.order;
|
|
}
|
|
|
|
return left.label.localeCompare(right.label);
|
|
});
|
|
}
|
|
|
|
function emptyNormalizationMetadata(): NormalizationMetadata {
|
|
return {
|
|
parserEngine: 'unknown',
|
|
regime: 'unknown',
|
|
fiscalPack: null,
|
|
parserVersion: '0.0.0',
|
|
surfaceRowCount: 0,
|
|
detailRowCount: 0,
|
|
kpiRowCount: 0,
|
|
unmappedRowCount: 0,
|
|
materialUnmappedRowCount: 0,
|
|
warnings: []
|
|
};
|
|
}
|
|
|
|
function buildNormalizationMetadata(
|
|
snapshots: FilingTaxonomySnapshotRecord[]
|
|
): NormalizationMetadata {
|
|
const latestSnapshot = snapshots[snapshots.length - 1];
|
|
if (!latestSnapshot) {
|
|
return emptyNormalizationMetadata();
|
|
}
|
|
|
|
return {
|
|
parserEngine: latestSnapshot.parser_engine,
|
|
regime: latestSnapshot.taxonomy_regime,
|
|
fiscalPack: latestSnapshot.fiscal_pack,
|
|
parserVersion: latestSnapshot.parser_version,
|
|
surfaceRowCount: snapshots.reduce(
|
|
(sum, snapshot) => sum + (snapshot.normalization_summary?.surfaceRowCount ?? 0),
|
|
0
|
|
),
|
|
detailRowCount: snapshots.reduce(
|
|
(sum, snapshot) => sum + (snapshot.normalization_summary?.detailRowCount ?? 0),
|
|
0
|
|
),
|
|
kpiRowCount: snapshots.reduce(
|
|
(sum, snapshot) => sum + (snapshot.normalization_summary?.kpiRowCount ?? 0),
|
|
0
|
|
),
|
|
unmappedRowCount: snapshots.reduce(
|
|
(sum, snapshot) => sum + (snapshot.normalization_summary?.unmappedRowCount ?? 0),
|
|
0
|
|
),
|
|
materialUnmappedRowCount: snapshots.reduce(
|
|
(sum, snapshot) => sum + (snapshot.normalization_summary?.materialUnmappedRowCount ?? 0),
|
|
0
|
|
),
|
|
warnings: [...new Set(snapshots.flatMap((snapshot) => snapshot.normalization_summary?.warnings ?? []))]
|
|
.sort((left, right) => left.localeCompare(right))
|
|
};
|
|
}
|
|
|
|
function rowHasValues(values: Record<string, number | null>) {
|
|
return Object.values(values).some((value) => value !== null);
|
|
}
|
|
|
|
function detailConceptIdentity(row: DetailFinancialRow) {
|
|
const localName = row.localName.trim().toLowerCase();
|
|
if (localName.length > 0) {
|
|
if (row.isExtension) {
|
|
return `extension:${localName}`;
|
|
}
|
|
|
|
if (row.namespaceUri.includes('us-gaap')) {
|
|
return `us-gaap:${localName}`;
|
|
}
|
|
|
|
if (row.namespaceUri.includes('ifrs')) {
|
|
return `ifrs:${localName}`;
|
|
}
|
|
|
|
const prefix = row.qname.split(':')[0]?.trim().toLowerCase();
|
|
if (prefix) {
|
|
return `${prefix}:${localName}`;
|
|
}
|
|
}
|
|
|
|
const normalizedQName = row.qname.trim().toLowerCase();
|
|
if (normalizedQName.length > 0) {
|
|
return normalizedQName;
|
|
}
|
|
|
|
const normalizedConceptKey = row.conceptKey.trim().toLowerCase();
|
|
if (normalizedConceptKey.length > 0) {
|
|
return normalizedConceptKey;
|
|
}
|
|
|
|
return row.key.trim().toLowerCase();
|
|
}
|
|
|
|
function detailMergeKey(row: DetailFinancialRow) {
|
|
const dimensionsKey = [...row.dimensionsSummary]
|
|
.map((value) => value.trim().toLowerCase())
|
|
.filter((value) => value.length > 0)
|
|
.sort((left, right) => left.localeCompare(right))
|
|
.join('|') || 'no-dimensions';
|
|
|
|
return [
|
|
detailConceptIdentity(row),
|
|
row.unit ?? 'no-unit',
|
|
dimensionsKey
|
|
].join('::');
|
|
}
|
|
|
|
const PINNED_INCOME_SURFACE_ROWS = new Set([
|
|
'revenue',
|
|
'gross_profit',
|
|
'operating_expenses',
|
|
'selling_general_and_administrative',
|
|
'research_and_development',
|
|
'other_operating_expense',
|
|
'operating_income',
|
|
'income_tax_expense',
|
|
'net_income'
|
|
]);
|
|
|
|
function shouldRetainSurfaceRow(
|
|
statement: FinancialStatementKind,
|
|
row: SurfaceFinancialRow,
|
|
values: Record<string, number | null>
|
|
) {
|
|
if (rowHasValues(values)) {
|
|
return true;
|
|
}
|
|
|
|
return statement === 'income' && PINNED_INCOME_SURFACE_ROWS.has(row.key);
|
|
}
|
|
|
|
function aggregateSurfaceRows(input: {
|
|
snapshots: FilingTaxonomySnapshotRecord[];
|
|
statement: FinancialStatementKind;
|
|
selectedPeriodIds: Set<string>;
|
|
}) {
|
|
const rowMap = new Map<string, SurfaceFinancialRow>();
|
|
|
|
for (const snapshot of input.snapshots) {
|
|
const rows = snapshot.surface_rows?.[input.statement] ?? [];
|
|
for (const row of rows) {
|
|
const sourceConcepts = row.sourceConcepts ?? [];
|
|
const sourceRowKeys = row.sourceRowKeys ?? [];
|
|
const sourceFactIds = row.sourceFactIds ?? [];
|
|
const rowValues = row.values ?? {};
|
|
const filteredValues = Object.fromEntries(
|
|
Object.entries(rowValues).filter(([periodId]) => input.selectedPeriodIds.has(periodId))
|
|
);
|
|
const filteredResolvedSourceRowKeys = Object.fromEntries(
|
|
Object.entries(row.resolvedSourceRowKeys ?? {}).filter(([periodId]) => input.selectedPeriodIds.has(periodId))
|
|
);
|
|
if (!shouldRetainSurfaceRow(input.statement, row, filteredValues)) {
|
|
continue;
|
|
}
|
|
|
|
const existing = rowMap.get(row.key);
|
|
if (!existing) {
|
|
rowMap.set(row.key, {
|
|
...row,
|
|
values: filteredValues,
|
|
resolvedSourceRowKeys: filteredResolvedSourceRowKeys,
|
|
sourceConcepts: [...sourceConcepts],
|
|
sourceRowKeys: [...sourceRowKeys],
|
|
sourceFactIds: [...sourceFactIds],
|
|
warningCodes: row.warningCodes ? [...row.warningCodes] : undefined
|
|
});
|
|
continue;
|
|
}
|
|
|
|
for (const [periodId, value] of Object.entries(filteredValues)) {
|
|
if (!(periodId in existing.values)) {
|
|
existing.values[periodId] = value;
|
|
}
|
|
}
|
|
|
|
for (const [periodId, sourceRowKey] of Object.entries(filteredResolvedSourceRowKeys)) {
|
|
if (!(periodId in existing.resolvedSourceRowKeys)) {
|
|
existing.resolvedSourceRowKeys[periodId] = sourceRowKey;
|
|
}
|
|
}
|
|
|
|
existing.sourceConcepts = [...new Set([...existing.sourceConcepts, ...sourceConcepts])].sort((left, right) => left.localeCompare(right));
|
|
existing.sourceRowKeys = [...new Set([...existing.sourceRowKeys, ...sourceRowKeys])].sort((left, right) => left.localeCompare(right));
|
|
existing.sourceFactIds = [...new Set([...existing.sourceFactIds, ...sourceFactIds])].sort((left, right) => left - right);
|
|
existing.hasDimensions = existing.hasDimensions || row.hasDimensions;
|
|
existing.order = Math.min(existing.order, row.order);
|
|
existing.detailCount = Math.max(existing.detailCount ?? 0, row.detailCount ?? 0);
|
|
existing.formulaKey = existing.formulaKey ?? row.formulaKey;
|
|
existing.statement = existing.statement ?? row.statement;
|
|
existing.resolutionMethod = existing.resolutionMethod ?? row.resolutionMethod;
|
|
existing.confidence = existing.confidence ?? row.confidence;
|
|
existing.warningCodes = [...new Set([...(existing.warningCodes ?? []), ...(row.warningCodes ?? [])])]
|
|
.sort((left, right) => left.localeCompare(right));
|
|
}
|
|
}
|
|
|
|
return [...rowMap.values()].sort((left, right) => {
|
|
if (left.order !== right.order) {
|
|
return left.order - right.order;
|
|
}
|
|
|
|
return left.label.localeCompare(right.label);
|
|
});
|
|
}
|
|
|
|
function aggregateDetailRows(input: {
|
|
snapshots: FilingTaxonomySnapshotRecord[];
|
|
statement: FinancialStatementKind;
|
|
selectedPeriodIds: Set<string>;
|
|
}) {
|
|
const detailBuckets = new Map<string, Map<string, DetailFinancialRow>>();
|
|
|
|
for (const snapshot of input.snapshots) {
|
|
const groups = snapshot.detail_rows?.[input.statement] ?? {};
|
|
for (const [surfaceKey, rows] of Object.entries(groups)) {
|
|
let bucket = detailBuckets.get(surfaceKey);
|
|
if (!bucket) {
|
|
bucket = new Map<string, DetailFinancialRow>();
|
|
detailBuckets.set(surfaceKey, bucket);
|
|
}
|
|
|
|
for (const row of rows) {
|
|
const sourceFactIds = row.sourceFactIds ?? [];
|
|
const dimensionsSummary = row.dimensionsSummary ?? [];
|
|
const rowValues = row.values ?? {};
|
|
const filteredValues = Object.fromEntries(
|
|
Object.entries(rowValues).filter(([periodId]) => input.selectedPeriodIds.has(periodId))
|
|
);
|
|
if (!rowHasValues(filteredValues)) {
|
|
continue;
|
|
}
|
|
|
|
const mergeKey = detailMergeKey(row);
|
|
const existing = bucket.get(mergeKey);
|
|
if (!existing) {
|
|
bucket.set(mergeKey, {
|
|
...row,
|
|
values: filteredValues,
|
|
sourceFactIds: [...sourceFactIds],
|
|
dimensionsSummary: [...dimensionsSummary]
|
|
});
|
|
continue;
|
|
}
|
|
|
|
for (const [periodId, value] of Object.entries(filteredValues)) {
|
|
if (!(periodId in existing.values)) {
|
|
existing.values[periodId] = value;
|
|
}
|
|
}
|
|
|
|
existing.sourceFactIds = [...new Set([...existing.sourceFactIds, ...sourceFactIds])].sort((left, right) => left - right);
|
|
existing.dimensionsSummary = [...new Set([...existing.dimensionsSummary, ...dimensionsSummary])].sort((left, right) => left.localeCompare(right));
|
|
existing.isExtension = existing.isExtension || row.isExtension;
|
|
existing.residualFlag = existing.residualFlag || row.residualFlag;
|
|
}
|
|
}
|
|
}
|
|
|
|
return Object.fromEntries(
|
|
[...detailBuckets.entries()].map(([surfaceKey, bucket]) => [
|
|
surfaceKey,
|
|
[...bucket.values()].sort((left, right) => left.label.localeCompare(right.label))
|
|
])
|
|
) satisfies SurfaceDetailMap;
|
|
}
|
|
|
|
function buildLtmDetailRows(input: {
|
|
detailRows: SurfaceDetailMap;
|
|
quarterlyPeriods: FinancialStatementPeriod[];
|
|
ltmPeriods: FinancialStatementPeriod[];
|
|
statement: Extract<FinancialStatementKind, 'income' | 'balance' | 'cash_flow'>;
|
|
}) {
|
|
const sortedQuarterlyPeriods = [...input.quarterlyPeriods].sort(periodSorter);
|
|
|
|
return Object.fromEntries(
|
|
Object.entries(input.detailRows).map(([surfaceKey, rows]) => {
|
|
const ltmRows = rows
|
|
.map((row) => {
|
|
const values: Record<string, number | null> = {};
|
|
|
|
for (const ltmPeriod of input.ltmPeriods) {
|
|
const anchorIndex = sortedQuarterlyPeriods.findIndex((period) => `ltm:${period.id}` === ltmPeriod.id);
|
|
if (anchorIndex < 3) {
|
|
continue;
|
|
}
|
|
|
|
const slice = sortedQuarterlyPeriods.slice(anchorIndex - 3, anchorIndex + 1);
|
|
const sourceValues = slice.map((period) => row.values[period.id] ?? null);
|
|
values[ltmPeriod.id] = input.statement === 'balance'
|
|
? sourceValues[sourceValues.length - 1] ?? null
|
|
: sourceValues.some((value) => value === null)
|
|
? null
|
|
: sourceValues.reduce<number>((sum, value) => sum + (value ?? 0), 0);
|
|
}
|
|
|
|
return {
|
|
...row,
|
|
values
|
|
};
|
|
})
|
|
.filter((row) => rowHasValues(row.values));
|
|
|
|
return [surfaceKey, ltmRows];
|
|
})
|
|
) satisfies SurfaceDetailMap;
|
|
}
|
|
|
|
function buildQuarterlyStatementSurfaceRows(input: {
|
|
statement: Extract<FinancialStatementKind, 'income' | 'balance' | 'cash_flow'>;
|
|
sourcePeriods: FinancialStatementPeriod[];
|
|
selectedPeriodIds: Set<string>;
|
|
faithfulRows: TaxonomyStatementRow[];
|
|
facts: TaxonomyFactRow[];
|
|
snapshots: FilingTaxonomySnapshotRecord[];
|
|
}) {
|
|
const aggregatedRows = aggregateSurfaceRows({
|
|
snapshots: input.snapshots,
|
|
statement: input.statement,
|
|
selectedPeriodIds: input.selectedPeriodIds
|
|
});
|
|
|
|
if (aggregatedRows.length > 0) {
|
|
return aggregatedRows;
|
|
}
|
|
|
|
return buildStandardizedRows({
|
|
rows: input.faithfulRows,
|
|
statement: input.statement,
|
|
periods: input.sourcePeriods,
|
|
facts: input.facts
|
|
}) as SurfaceFinancialRow[];
|
|
}
|
|
|
|
function aggregatePersistedKpiRows(input: {
|
|
snapshots: FilingTaxonomySnapshotRecord[];
|
|
selectedPeriodIds: Set<string>;
|
|
}) {
|
|
const rowMap = new Map<string, StructuredKpiRow>();
|
|
|
|
for (const snapshot of input.snapshots) {
|
|
for (const row of snapshot.kpi_rows ?? []) {
|
|
const sourceConcepts = row.sourceConcepts ?? [];
|
|
const sourceFactIds = row.sourceFactIds ?? [];
|
|
const rowValues = row.values ?? {};
|
|
const filteredValues = Object.fromEntries(
|
|
Object.entries(rowValues).filter(([periodId]) => input.selectedPeriodIds.has(periodId))
|
|
);
|
|
if (!rowHasValues(filteredValues)) {
|
|
continue;
|
|
}
|
|
|
|
const existing = rowMap.get(row.key);
|
|
if (!existing) {
|
|
rowMap.set(row.key, {
|
|
...row,
|
|
values: filteredValues,
|
|
sourceConcepts: [...sourceConcepts],
|
|
sourceFactIds: [...sourceFactIds]
|
|
});
|
|
continue;
|
|
}
|
|
|
|
existing.values = {
|
|
...existing.values,
|
|
...filteredValues
|
|
};
|
|
existing.sourceConcepts = [...new Set([...existing.sourceConcepts, ...sourceConcepts])].sort((left, right) => left.localeCompare(right));
|
|
existing.sourceFactIds = [...new Set([...existing.sourceFactIds, ...sourceFactIds])].sort((left, right) => left - right);
|
|
existing.hasDimensions = existing.hasDimensions || row.hasDimensions;
|
|
}
|
|
}
|
|
|
|
return [...rowMap.values()].sort((left, right) => {
|
|
if (left.order !== right.order) {
|
|
return left.order - right.order;
|
|
}
|
|
|
|
return left.label.localeCompare(right.label);
|
|
});
|
|
}
|
|
|
|
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,
|
|
statementDetails: 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,
|
|
normalization: emptyNormalizationMetadata(),
|
|
dimensionBreakdown: null
|
|
} satisfies CompanyFinancialStatementsResponse;
|
|
}
|
|
|
|
async function buildStatementSurfaceBundle(input: {
|
|
surfaceKind: Extract<FinancialSurfaceKind, 'income_statement' | 'balance_sheet' | 'cash_flow_statement'>;
|
|
cadence: FinancialCadence;
|
|
sourcePeriods: FinancialStatementPeriod[];
|
|
targetPeriods: FinancialStatementPeriod[];
|
|
selectedPeriodIds: Set<string>;
|
|
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
|
|
&& Array.isArray((cached as Partial<StandardizedStatementBundlePayload>).rows)
|
|
&& typeof (cached as Partial<StandardizedStatementBundlePayload>).detailRows === 'object'
|
|
) {
|
|
return cached as StandardizedStatementBundlePayload;
|
|
}
|
|
|
|
const statement = surfaceToStatementKind(input.surfaceKind);
|
|
if (!statement || (statement !== 'income' && statement !== 'balance' && statement !== 'cash_flow')) {
|
|
return {
|
|
rows: [],
|
|
detailRows: {},
|
|
trendSeries: [],
|
|
categories: [],
|
|
normalization: buildNormalizationMetadata(input.snapshots)
|
|
} satisfies StandardizedStatementBundlePayload;
|
|
}
|
|
|
|
const quarterlyRows = buildQuarterlyStatementSurfaceRows({
|
|
statement,
|
|
sourcePeriods: input.sourcePeriods,
|
|
selectedPeriodIds: input.selectedPeriodIds,
|
|
faithfulRows: input.faithfulRows,
|
|
facts: input.facts,
|
|
snapshots: input.snapshots
|
|
});
|
|
const quarterlyDetailRows = aggregateDetailRows({
|
|
snapshots: input.snapshots,
|
|
statement,
|
|
selectedPeriodIds: input.selectedPeriodIds
|
|
});
|
|
const rows = input.cadence === 'ltm'
|
|
? buildLtmStandardizedRows(quarterlyRows, input.sourcePeriods, input.targetPeriods, statement) as SurfaceFinancialRow[]
|
|
: quarterlyRows;
|
|
const detailRows = input.cadence === 'ltm'
|
|
? buildLtmDetailRows({
|
|
detailRows: quarterlyDetailRows,
|
|
quarterlyPeriods: input.sourcePeriods,
|
|
ltmPeriods: input.targetPeriods,
|
|
statement
|
|
})
|
|
: quarterlyDetailRows;
|
|
const normalization = buildNormalizationMetadata(input.snapshots);
|
|
|
|
const payload = {
|
|
rows,
|
|
detailRows,
|
|
trendSeries: buildTrendSeries({
|
|
surfaceKind: input.surfaceKind,
|
|
statementRows: rows
|
|
}),
|
|
categories: buildFinancialCategories(rows, input.surfaceKind),
|
|
normalization
|
|
} satisfies StandardizedStatementBundlePayload;
|
|
|
|
await writeFinancialBundle({
|
|
ticker: input.snapshots[0]?.ticker ?? '',
|
|
surfaceKind: input.surfaceKind,
|
|
cadence: input.cadence,
|
|
snapshots: input.snapshots,
|
|
payload: payload as unknown as Record<string, unknown>
|
|
});
|
|
|
|
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<CompanyFinancialStatementsResponse, 'ratioRows' | 'trendSeries' | 'categories'>;
|
|
}
|
|
|
|
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<CompanyFinancialStatementsResponse, 'ratioRows' | 'trendSeries' | 'categories'>;
|
|
|
|
await writeFinancialBundle({
|
|
ticker: input.ticker,
|
|
surfaceKind: 'ratios',
|
|
cadence: input.cadence,
|
|
snapshots: input.snapshots,
|
|
payload: payload as unknown as Record<string, unknown>
|
|
});
|
|
|
|
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<CompanyFinancialStatementsResponse, 'kpiRows' | 'trendSeries' | 'categories'>;
|
|
}
|
|
|
|
const persistedRows = aggregatePersistedKpiRows({
|
|
snapshots: input.snapshots,
|
|
selectedPeriodIds: new Set(input.periods.map((period) => period.id))
|
|
});
|
|
const resolved = resolveKpiDefinitions(input.ticker);
|
|
if (!resolved.template) {
|
|
return {
|
|
kpiRows: persistedRows,
|
|
trendSeries: buildTrendSeries({
|
|
surfaceKind: 'segments_kpis',
|
|
kpiRows: persistedRows
|
|
}),
|
|
categories: buildFinancialCategories(persistedRows, 'segments_kpis')
|
|
};
|
|
}
|
|
|
|
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 kpiRows = mergeStructuredKpiRowsByPriority([
|
|
persistedRows,
|
|
taxonomyRows,
|
|
noteRows
|
|
]);
|
|
|
|
const payload = {
|
|
kpiRows,
|
|
trendSeries: buildTrendSeries({
|
|
surfaceKind: 'segments_kpis',
|
|
kpiRows
|
|
}),
|
|
categories: buildFinancialCategories(kpiRows, 'segments_kpis')
|
|
} satisfies Pick<CompanyFinancialStatementsResponse, 'kpiRows' | 'trendSeries' | 'categories'>;
|
|
|
|
await writeFinancialBundle({
|
|
ticker: input.ticker,
|
|
surfaceKind: 'segments_kpis',
|
|
cadence: input.cadence,
|
|
snapshots: input.snapshots,
|
|
payload: payload as unknown as Record<string, unknown>
|
|
});
|
|
|
|
return payload;
|
|
}
|
|
|
|
export function defaultFinancialSyncLimit() {
|
|
return 60;
|
|
}
|
|
|
|
export async function getCompanyFinancials(input: GetCompanyFinancialsInput): Promise<CompanyFinancialStatementsResponse> {
|
|
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
|
|
},
|
|
normalization: buildNormalizationMetadata(snapshotResult.snapshots)
|
|
};
|
|
}
|
|
|
|
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 baseFaithfulRows = buildRows(selection.snapshots, statement, selection.selectedPeriodIds);
|
|
const faithfulRows = input.cadence === 'ltm'
|
|
? buildLtmFaithfulRows(
|
|
baseFaithfulRows,
|
|
selection.periods,
|
|
periods,
|
|
statement
|
|
)
|
|
: baseFaithfulRows;
|
|
|
|
const factsForStatement = allFacts.facts.filter((fact) => fact.statement === statement);
|
|
const factsForStandardization = allFacts.facts;
|
|
const standardizedPayload = await buildStatementSurfaceBundle({
|
|
surfaceKind: input.surfaceKind as Extract<FinancialSurfaceKind, 'income_statement' | 'balance_sheet' | 'cash_flow_statement'>,
|
|
cadence: input.cadence,
|
|
sourcePeriods: selection.periods,
|
|
targetPeriods: periods,
|
|
selectedPeriodIds: selection.selectedPeriodIds,
|
|
faithfulRows: baseFaithfulRows,
|
|
facts: factsForStandardization,
|
|
snapshots: selection.snapshots
|
|
});
|
|
|
|
const standardizedRows = 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
|
|
},
|
|
statementDetails: standardizedPayload.detailRows,
|
|
ratioRows: null,
|
|
kpiRows: null,
|
|
trendSeries: standardizedPayload.trendSeries,
|
|
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,
|
|
normalization: standardizedPayload.normalization,
|
|
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 = buildQuarterlyStatementSurfaceRows({
|
|
statement: 'income',
|
|
sourcePeriods: incomeSelection.periods,
|
|
selectedPeriodIds: incomeSelection.selectedPeriodIds,
|
|
faithfulRows: buildRows(incomeSelection.snapshots, 'income', incomeSelection.selectedPeriodIds),
|
|
facts: allFacts.facts,
|
|
snapshots: incomeSelection.snapshots
|
|
});
|
|
const balanceQuarterlyRows = rekeyRowsByFilingId(buildQuarterlyStatementSurfaceRows({
|
|
statement: 'balance',
|
|
sourcePeriods: balanceSelection.periods,
|
|
selectedPeriodIds: balanceSelection.selectedPeriodIds,
|
|
faithfulRows: buildRows(balanceSelection.snapshots, 'balance', balanceSelection.selectedPeriodIds),
|
|
facts: allFacts.facts,
|
|
snapshots: balanceSelection.snapshots
|
|
}), balanceSelection.periods, incomeSelection.periods);
|
|
const cashFlowQuarterlyRows = rekeyRowsByFilingId(buildQuarterlyStatementSurfaceRows({
|
|
statement: 'cash_flow',
|
|
sourcePeriods: cashFlowSelection.periods,
|
|
selectedPeriodIds: cashFlowSelection.selectedPeriodIds,
|
|
faithfulRows: buildRows(cashFlowSelection.snapshots, 'cash_flow', cashFlowSelection.selectedPeriodIds),
|
|
facts: allFacts.facts,
|
|
snapshots: cashFlowSelection.snapshots
|
|
}), 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,
|
|
statementDetails: 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,
|
|
normalization: buildNormalizationMetadata(incomeSelection.snapshots),
|
|
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,
|
|
statementDetails: 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,
|
|
normalization: buildNormalizationMetadata(incomeSelection.snapshots),
|
|
dimensionBreakdown: mergeDimensionBreakdownMaps(kpiBreakdown)
|
|
};
|
|
}
|
|
|
|
export async function getCompanyFinancialTaxonomy(input: GetCompanyFinancialsInput) {
|
|
return await getCompanyFinancials(input);
|
|
}
|
|
|
|
export const __financialTaxonomyInternals = {
|
|
buildRows,
|
|
buildStandardizedRows,
|
|
buildDimensionBreakdown,
|
|
buildNormalizationMetadata,
|
|
aggregateSurfaceRows,
|
|
aggregateDetailRows,
|
|
mergeStructuredKpiRowsByPriority,
|
|
periodSorter,
|
|
selectPrimaryPeriodsByCadence,
|
|
buildLtmPeriods,
|
|
buildLtmFaithfulRows,
|
|
buildLtmStandardizedRows
|
|
};
|