- Add bundled financial modeling pipeline (ratios, KPI dimensions/notes, trend series, standardization) - Introduce company financial bundles storage (Drizzle migration + repo wiring) - Refactor financials page/API/query flow to use surfaceKind + cadence and new response shapes
82 lines
3.5 KiB
TypeScript
82 lines
3.5 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
import type {
|
|
FinancialCadence,
|
|
FinancialStatementPeriod,
|
|
StandardizedFinancialRow
|
|
} from '@/lib/types';
|
|
import { buildRatioRows } from './ratios';
|
|
|
|
function createPeriod(id: string, filingId: number, filingDate: string, periodEnd: string): FinancialStatementPeriod {
|
|
return {
|
|
id,
|
|
filingId,
|
|
accessionNumber: `0000-${filingId}`,
|
|
filingDate,
|
|
periodStart: '2025-01-01',
|
|
periodEnd,
|
|
filingType: '10-Q',
|
|
periodLabel: id
|
|
};
|
|
}
|
|
|
|
function createRow(key: string, values: Record<string, number | null>): StandardizedFinancialRow {
|
|
return {
|
|
key,
|
|
label: key,
|
|
category: 'test',
|
|
order: 10,
|
|
unit: 'currency',
|
|
values,
|
|
sourceConcepts: [`us-gaap:${key}`],
|
|
sourceRowKeys: [key],
|
|
sourceFactIds: [1],
|
|
formulaKey: null,
|
|
hasDimensions: false,
|
|
resolvedSourceRowKeys: Object.fromEntries(Object.keys(values).map((periodId) => [periodId, key]))
|
|
};
|
|
}
|
|
|
|
describe('ratio engine', () => {
|
|
it('nulls valuation ratios when price data is unavailable', () => {
|
|
const periods = [createPeriod('2025-q4', 1, '2026-01-31', '2025-12-31')];
|
|
const rows = {
|
|
income: [createRow('revenue', { '2025-q4': 100 }), createRow('diluted_eps', { '2025-q4': 2 }), createRow('ebitda', { '2025-q4': 20 }), createRow('net_income', { '2025-q4': 10 })],
|
|
balance: [createRow('total_equity', { '2025-q4': 50 }), createRow('total_debt', { '2025-q4': 30 }), createRow('cash_and_equivalents', { '2025-q4': 5 }), createRow('short_term_investments', { '2025-q4': 5 }), createRow('diluted_shares', { '2025-q4': 10 })],
|
|
cashFlow: [createRow('free_cash_flow', { '2025-q4': 12 })]
|
|
};
|
|
|
|
const ratioRows = buildRatioRows({
|
|
periods,
|
|
cadence: 'quarterly' satisfies FinancialCadence,
|
|
rows,
|
|
pricesByPeriodId: { '2025-q4': null }
|
|
});
|
|
|
|
expect(ratioRows.find((row) => row.key === 'market_cap')?.values['2025-q4']).toBeNull();
|
|
expect(ratioRows.find((row) => row.key === 'price_to_earnings')?.values['2025-q4']).toBeNull();
|
|
expect(ratioRows.find((row) => row.key === 'ev_to_sales')?.values['2025-q4']).toBeNull();
|
|
});
|
|
|
|
it('nulls ratios on zero denominators', () => {
|
|
const periods = [
|
|
createPeriod('2024-q4', 1, '2025-01-31', '2024-12-31'),
|
|
createPeriod('2025-q4', 2, '2026-01-31', '2025-12-31')
|
|
];
|
|
const rows = {
|
|
income: [createRow('net_income', { '2024-q4': 5, '2025-q4': 10 }), createRow('revenue', { '2024-q4': 100, '2025-q4': 120 }), createRow('diluted_eps', { '2024-q4': 1, '2025-q4': 2 }), createRow('ebitda', { '2024-q4': 10, '2025-q4': 12 })],
|
|
balance: [createRow('total_equity', { '2024-q4': 0, '2025-q4': 0 }), createRow('total_assets', { '2024-q4': 50, '2025-q4': 60 }), createRow('total_debt', { '2024-q4': 20, '2025-q4': 25 }), createRow('cash_and_equivalents', { '2024-q4': 2, '2025-q4': 3 }), createRow('short_term_investments', { '2024-q4': 1, '2025-q4': 1 }), createRow('current_assets', { '2024-q4': 10, '2025-q4': 12 }), createRow('current_liabilities', { '2024-q4': 0, '2025-q4': 0 }), createRow('diluted_shares', { '2024-q4': 10, '2025-q4': 10 })],
|
|
cashFlow: [createRow('free_cash_flow', { '2024-q4': 6, '2025-q4': 7 })]
|
|
};
|
|
|
|
const ratioRows = buildRatioRows({
|
|
periods,
|
|
cadence: 'quarterly',
|
|
rows,
|
|
pricesByPeriodId: { '2024-q4': 10, '2025-q4': 12 }
|
|
});
|
|
|
|
expect(ratioRows.find((row) => row.key === 'debt_to_equity')?.values['2025-q4']).toBeNull();
|
|
expect(ratioRows.find((row) => row.key === 'current_ratio')?.values['2025-q4']).toBeNull();
|
|
});
|
|
});
|