Add research workspace and graphing flows
This commit is contained in:
225
lib/graphing/series.test.ts
Normal file
225
lib/graphing/series.test.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { buildGraphingComparisonData } from '@/lib/graphing/series';
|
||||
import type {
|
||||
CompanyFinancialStatementsResponse,
|
||||
FinancialStatementPeriod,
|
||||
RatioRow,
|
||||
StandardizedFinancialRow
|
||||
} 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: StandardizedFinancialRow['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]))
|
||||
} satisfies StandardizedFinancialRow;
|
||||
}
|
||||
|
||||
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?: StandardizedFinancialRow[];
|
||||
ratioRows?: RatioRow[];
|
||||
}) {
|
||||
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 ?? []
|
||||
},
|
||||
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
|
||||
},
|
||||
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('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',
|
||||
latestValue: 70,
|
||||
priorValue: 50,
|
||||
changeValue: 20,
|
||||
latestDateKey: '2025-03-31'
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user