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, 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, 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: { regime: 'unknown', fiscalPack: input.fiscalPack ?? null, parserVersion: '0.0.0', unmappedRowCount: 0, materialUnmappedRowCount: 0 }, 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' }); }); });