Files
Neon-Desk/lib/server/financials/ratios.test.ts
francy51 db01f207a5 Expand financials surfaces with ratios, KPIs, and cadence support
- 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
2026-03-07 15:16:35 -05:00

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();
});
});