273 lines
8.0 KiB
TypeScript
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'
|
|
});
|
|
});
|
|
});
|