Files
Neon-Desk/lib/graphing/series.test.ts

273 lines
8.0 KiB
TypeScript

import { describe, expect, it } from 'bun:test';
import { buildGraphingComparisonData } from '@/lib/graphing/series';
import type {
CompanyFinancialStatementsResponse,
FinancialStatementPeriod,
RatioRow,
SurfaceFinancialRow
} from '@/lib/types';
function createPeriod(input: {
id: string;
filingId: number;
filingDate: string;
periodEnd: string;
filingType?: '10-K' | '10-Q';
}) {
return {
id: input.id,
filingId: input.filingId,
accessionNumber: `0000-${input.filingId}`,
filingDate: input.filingDate,
periodStart: '2025-01-01',
periodEnd: input.periodEnd,
filingType: input.filingType ?? '10-Q',
periodLabel: input.id
} satisfies FinancialStatementPeriod;
}
function createStatementRow(key: string, values: Record<string, number | null>, unit: SurfaceFinancialRow['unit'] = 'currency') {
return {
key,
label: key,
category: 'test',
order: 10,
unit,
values,
sourceConcepts: [key],
sourceRowKeys: [key],
sourceFactIds: [1],
formulaKey: null,
hasDimensions: false,
resolvedSourceRowKeys: Object.fromEntries(Object.keys(values).map((periodId) => [periodId, key])),
statement: 'income'
} satisfies SurfaceFinancialRow;
}
function createRatioRow(key: string, values: Record<string, number | null>, unit: RatioRow['unit'] = 'percent') {
return {
...createStatementRow(key, values, unit),
denominatorKey: null
} satisfies RatioRow;
}
function createFinancials(input: {
ticker: string;
companyName: string;
periods: FinancialStatementPeriod[];
statementRows?: SurfaceFinancialRow[];
ratioRows?: RatioRow[];
fiscalPack?: string | null;
}) {
return {
company: {
ticker: input.ticker,
companyName: input.companyName,
cik: null
},
surfaceKind: 'income_statement',
cadence: 'annual',
displayModes: ['standardized'],
defaultDisplayMode: 'standardized',
periods: input.periods,
statementRows: {
faithful: [],
standardized: input.statementRows ?? []
},
statementDetails: null,
ratioRows: input.ratioRows ?? [],
kpiRows: null,
trendSeries: [],
categories: [],
availability: {
adjusted: false,
customMetrics: false
},
nextCursor: null,
facts: null,
coverage: {
filings: input.periods.length,
rows: input.statementRows?.length ?? 0,
dimensions: 0,
facts: 0
},
dataSourceStatus: {
enabled: true,
hydratedFilings: input.periods.length,
partialFilings: 0,
failedFilings: 0,
pendingFilings: 0,
queuedSync: false
},
metrics: {
taxonomy: null,
validation: null
},
normalization: {
parserEngine: 'fiscal-xbrl',
regime: 'unknown',
fiscalPack: input.fiscalPack ?? null,
parserVersion: '0.0.0',
surfaceRowCount: 0,
detailRowCount: 0,
kpiRowCount: 0,
unmappedRowCount: 0,
materialUnmappedRowCount: 0,
warnings: []
},
dimensionBreakdown: null
} satisfies CompanyFinancialStatementsResponse;
}
describe('graphing series', () => {
it('aligns multiple companies onto a union date axis', () => {
const data = buildGraphingComparisonData({
surface: 'income_statement',
metric: 'revenue',
results: [
{
ticker: 'MSFT',
financials: createFinancials({
ticker: 'MSFT',
companyName: 'Microsoft',
periods: [
createPeriod({ id: 'msft-q1', filingId: 1, filingDate: '2025-01-30', periodEnd: '2024-12-31' }),
createPeriod({ id: 'msft-q2', filingId: 2, filingDate: '2025-04-28', periodEnd: '2025-03-31' })
],
statementRows: [createStatementRow('revenue', { 'msft-q1': 100, 'msft-q2': 120 })]
})
},
{
ticker: 'AAPL',
financials: createFinancials({
ticker: 'AAPL',
companyName: 'Apple',
periods: [
createPeriod({ id: 'aapl-q1', filingId: 3, filingDate: '2025-02-02', periodEnd: '2025-01-31' }),
createPeriod({ id: 'aapl-q2', filingId: 4, filingDate: '2025-04-30', periodEnd: '2025-03-31' })
],
statementRows: [createStatementRow('revenue', { 'aapl-q1': 90, 'aapl-q2': 130 })]
})
}
]
});
expect(data.chartData.map((entry) => entry.dateKey)).toEqual([
'2024-12-31',
'2025-01-31',
'2025-03-31'
]);
expect(data.chartData[0]?.MSFT).toBe(100);
expect(data.chartData[0]?.AAPL).toBeNull();
expect(data.chartData[1]?.AAPL).toBe(90);
});
it('preserves partial failures without blanking the whole chart', () => {
const data = buildGraphingComparisonData({
surface: 'income_statement',
metric: 'revenue',
results: [
{
ticker: 'MSFT',
financials: createFinancials({
ticker: 'MSFT',
companyName: 'Microsoft',
periods: [createPeriod({ id: 'msft-q1', filingId: 1, filingDate: '2025-01-30', periodEnd: '2024-12-31' })],
statementRows: [createStatementRow('revenue', { 'msft-q1': 100 })]
})
},
{
ticker: 'FAIL',
error: 'Ticker not found'
}
]
});
expect(data.hasAnyData).toBe(true);
expect(data.hasPartialData).toBe(true);
expect(data.latestRows.find((row) => row.ticker === 'FAIL')?.status).toBe('error');
});
it('marks companies with missing metric values as no metric data', () => {
const data = buildGraphingComparisonData({
surface: 'ratios',
metric: 'gross_margin',
results: [
{
ticker: 'NVDA',
financials: createFinancials({
ticker: 'NVDA',
companyName: 'NVIDIA',
periods: [createPeriod({ id: 'nvda-q1', filingId: 1, filingDate: '2025-01-30', periodEnd: '2024-12-31' })],
ratioRows: [createRatioRow('gross_margin', { 'nvda-q1': null })]
})
}
]
});
expect(data.latestRows[0]?.status).toBe('no_metric_data');
expect(data.hasAnyData).toBe(false);
});
it('marks not meaningful standardized rows separately from missing metric data', () => {
const data = buildGraphingComparisonData({
surface: 'income_statement',
metric: 'gross_profit',
results: [
{
ticker: 'JPM',
financials: createFinancials({
ticker: 'JPM',
companyName: 'JPMorgan Chase & Co.',
fiscalPack: 'bank_lender',
periods: [createPeriod({ id: 'jpm-fy', filingId: 1, filingDate: '2026-02-13', periodEnd: '2025-12-31', filingType: '10-K' })],
statementRows: [{
...createStatementRow('gross_profit', { 'jpm-fy': null }),
resolutionMethod: 'not_meaningful'
}]
})
}
]
});
expect(data.latestRows[0]).toMatchObject({
ticker: 'JPM',
fiscalPack: 'bank_lender',
status: 'not_meaningful',
errorMessage: 'Not meaningful for this pack.'
});
expect(data.hasAnyData).toBe(false);
expect(data.hasPartialData).toBe(true);
});
it('derives latest and prior values for the summary table', () => {
const data = buildGraphingComparisonData({
surface: 'income_statement',
metric: 'revenue',
results: [
{
ticker: 'AMD',
financials: createFinancials({
ticker: 'AMD',
companyName: 'AMD',
periods: [
createPeriod({ id: 'amd-q1', filingId: 1, filingDate: '2025-01-30', periodEnd: '2024-12-31' }),
createPeriod({ id: 'amd-q2', filingId: 2, filingDate: '2025-04-30', periodEnd: '2025-03-31' })
],
statementRows: [createStatementRow('revenue', { 'amd-q1': 50, 'amd-q2': 70 })]
})
}
]
});
expect(data.latestRows[0]).toMatchObject({
ticker: 'AMD',
fiscalPack: null,
latestValue: 70,
priorValue: 50,
changeValue: 20,
latestDateKey: '2025-03-31'
});
});
});