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(`
+
+
+
+ `);
+
+ 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(`
+
+
+
+ `, { 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);
+ });
+});