From cf6f26fa1510c2138653b64f39a89b159e9e0594 Mon Sep 17 00:00:00 2001 From: francy51 Date: Mon, 2 Mar 2026 09:34:51 -0500 Subject: [PATCH] test(financials-v2): cover statement parsing and aggregation internals --- lib/server/financial-statements.test.ts | 139 ++++++++++++++++++++++++ lib/server/sec.test.ts | 132 ++++++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 lib/server/financial-statements.test.ts diff --git a/lib/server/financial-statements.test.ts b/lib/server/financial-statements.test.ts new file mode 100644 index 0000000..f0c40f3 --- /dev/null +++ b/lib/server/financial-statements.test.ts @@ -0,0 +1,139 @@ +import { describe, expect, it } from 'bun:test'; +import { __financialStatementsInternals } from './financial-statements'; +import type { FilingStatementSnapshotRecord } from '@/lib/server/repos/filing-statements'; + +function sampleSnapshot(): FilingStatementSnapshotRecord { + return { + id: 10, + filing_id: 44, + ticker: 'MSFT', + filing_date: '2025-12-31', + filing_type: '10-K', + period_end: '2025-12-31', + statement_bundle: { + periods: [ + { + id: '2025-12-31-0001', + filingId: 44, + accessionNumber: '0001', + filingDate: '2025-12-31', + periodEnd: '2025-12-31', + filingType: '10-K', + periodLabel: 'Fiscal Year End' + } + ], + statements: { + income: [ + { + key: 'revenue-line', + label: 'Revenue', + concept: 'us-gaap:Revenues', + order: 1, + depth: 0, + isSubtotal: false, + values: { '2025-12-31-0001': 120_000 } + } + ], + balance: [], + cash_flow: [], + equity: [], + comprehensive_income: [] + } + }, + standardized_bundle: { + periods: [ + { + id: '2025-12-31-0001', + filingId: 44, + accessionNumber: '0001', + filingDate: '2025-12-31', + periodEnd: '2025-12-31', + filingType: '10-K', + periodLabel: 'Fiscal Year End' + } + ], + statements: { + income: [ + { + key: 'revenue', + label: 'Revenue', + concept: 'us-gaap:Revenues', + category: 'core', + sourceConcepts: ['us-gaap:Revenues'], + values: { '2025-12-31-0001': 120_000 } + } + ], + balance: [], + cash_flow: [], + equity: [], + comprehensive_income: [] + } + }, + dimension_bundle: { + statements: { + income: [ + { + rowKey: 'revenue-line', + concept: 'us-gaap:Revenues', + periodId: '2025-12-31-0001', + axis: 'srt:StatementBusinessSegmentsAxis', + member: 'acme:CloudMember', + value: 55_000, + unit: 'USD' + } + ], + balance: [], + cash_flow: [], + equity: [], + comprehensive_income: [] + } + }, + parse_status: 'ready', + parse_error: null, + source: 'sec_filing_summary', + created_at: '2026-01-01T00:00:00.000Z', + updated_at: '2026-01-01T00:00:00.000Z' + }; +} + +describe('financial statements service internals', () => { + it('builds sorted periods for selected mode/statement', () => { + const snapshot = sampleSnapshot(); + + const periods = __financialStatementsInternals.buildPeriods( + [snapshot], + 'standardized', + 'income' + ); + + expect(periods.length).toBe(1); + expect(periods[0]?.id).toBe('2025-12-31-0001'); + }); + + it('builds standardized rows and includes dimensions when requested', () => { + const snapshot = sampleSnapshot(); + const periods = __financialStatementsInternals.buildPeriods( + [snapshot], + 'standardized', + 'income' + ); + + const result = __financialStatementsInternals.buildRows( + [snapshot], + periods, + 'standardized', + 'income', + true + ); + + expect(result.rows.length).toBe(1); + expect(result.rows[0]?.hasDimensions).toBe(true); + expect(result.dimensions).not.toBeNull(); + expect(result.dimensions?.['revenue-line']?.length).toBe(1); + }); + + it('returns default sync limits by window', () => { + expect(__financialStatementsInternals.defaultFinancialSyncLimit('10y')).toBe(60); + expect(__financialStatementsInternals.defaultFinancialSyncLimit('all')).toBe(120); + }); +}); diff --git a/lib/server/sec.test.ts b/lib/server/sec.test.ts index 8fa3c75..207a9a1 100644 --- a/lib/server/sec.test.ts +++ b/lib/server/sec.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it, mock } from 'bun:test'; import { + __statementInternals, fetchFilingMetricsForFilings, + hydrateFilingStatementSnapshot, fetchPrimaryFilingText, normalizeSecDocumentText, resolvePrimaryFilingUrl, @@ -190,3 +192,133 @@ describe('sec filing text helpers', () => { } }); }); + +describe('statement snapshot parsing', () => { + it('parses FilingSummary reports and statement rows with order/depth/subtotals', () => { + const reports = __statementInternals.parseFilingSummaryReports(` + + + Statements of Operations + Consolidated Statements of Operations + income.htm + + + `); + + expect(reports.length).toBe(1); + expect(reports[0]?.htmlFileName).toBe('income.htm'); + + const rows = __statementInternals.parseStatementRowsFromReport(` + + + + + + + + + + + + + + +
Revenue$120,000
Cost of Revenue(50,000)
Total Net Income25,000
+ + `); + + expect(rows.length).toBe(3); + expect(rows[0]?.label).toBe('Revenue'); + expect(rows[0]?.order).toBe(1); + expect(rows[1]?.depth).toBe(2); + expect(rows[1]?.value).toBe(-50_000); + expect(rows[2]?.isSubtotal).toBe(true); + }); + + it('extracts dimensional facts from inline XBRL contexts', () => { + const dimensions = __statementInternals.parseDimensionFacts(` + + 2025-12-31 + + us-gaap:ProductMember + + + 50000 + `, 'fallback-period'); + + expect(dimensions.income.length).toBe(1); + expect(dimensions.income[0]?.axis).toContain('ProductOrServiceAxis'); + expect(dimensions.income[0]?.member).toContain('ProductMember'); + expect(dimensions.income[0]?.periodId).toBe('2025-12-31'); + }); + + it('hydrates a filing snapshot with partial status when only one statement is found', async () => { + const fetchImpl = mock(async (input: RequestInfo | URL, _init?: RequestInit) => { + const url = String(input); + + if (url.endsWith('FilingSummary.xml')) { + return new Response(` + + + Statements of Operations + Consolidated Statements of Operations + income.htm + + + `, { status: 200 }); + } + + if (url.endsWith('income.htm')) { + return new Response(` + + + + + + + + + + +
Revenue120000
Net Income24000
+ + `, { status: 200 }); + } + + return new Response(` + + 2025-12-31 + + acme:EnterpriseMember + + + 120000 + `, { status: 200 }); + }) as unknown as typeof fetch; + + const snapshot = await hydrateFilingStatementSnapshot({ + filingId: 99, + ticker: 'MSFT', + cik: '0000789019', + accessionNumber: '0000789019-25-000001', + filingDate: '2025-12-31', + filingType: '10-K', + filingUrl: 'https://www.sec.gov/Archives/edgar/data/789019/000078901925000001/msft10k.htm', + primaryDocument: 'msft10k.htm', + metrics: { + revenue: 120_000, + netIncome: 24_000, + totalAssets: 450_000, + cash: 90_000, + debt: 110_000 + } + }, { + fetchImpl + }); + + expect(snapshot.parse_status).toBe('partial'); + expect(snapshot.statement_bundle?.statements.income.length).toBeGreaterThan(0); + expect(snapshot.standardized_bundle?.statements.income.find((row) => row.key === 'revenue')?.values).toBeDefined(); + expect(snapshot.dimension_bundle?.statements.income.length).toBeGreaterThan(0); + }); +});