import { describe, expect, test } from "bun:test"; import type { TickerFinancials } from "../../../../types/financials"; import { buildFinancialTableModel, computeGrowth, computeTTM, formatFinancialCell, financialStatementCurrency, resolveFinancialPeriodOption, resolveFinancialSubTabKey, } from "./model"; function createFinancials(): TickerFinancials { return { annualStatements: [ { date: "2024-12-31", totalRevenue: 100, netIncome: 25 }, { date: "2025-12-31", totalRevenue: 150, netIncome: 45 }, ], quarterlyStatements: [ { date: "2025-03-31", totalRevenue: 10, netIncome: 1 }, { date: "2025-06-30", totalRevenue: 20, netIncome: 2 }, { date: "2025-09-30", totalRevenue: 30, netIncome: 3 }, { date: "2025-12-31", totalRevenue: 40, netIncome: 4 }, ], priceHistory: [], }; } describe("financial statement table model", () => { test("large growth fits beside precise EPS without truncating its sign or percent unit", () => { const growth = computeGrowth(0.004125, -0.00000254)!; const cell = formatFinancialCell("0.004125", growth); expect(cell.valueText.trim()).toBe("0.004125"); expect(cell.growthText.trim()).toBe("+163k%"); expect(cell.valueText.length + cell.growthText.length).toBe(18); for (const rate of [-999.995, -100, 99.995, 9999.995, 1e12, -1e100, 1e100, -1e307, 1e307]) { const formatted = formatFinancialCell("0.004125", rate).growthText.trim(); expect(formatted.endsWith("%")).toBe(true); expect(formatted.length).toBeLessThanOrEqual(6); expect(formatted).toContain(rate < 0 ? "-" : formatted.startsWith(">") ? ">" : "+"); } expect(computeGrowth(Number.MAX_VALUE, Number.MIN_VALUE)).toBeUndefined(); expect(computeGrowth(Number.MAX_VALUE, -Number.MAX_VALUE)).toBe(2); expect(computeGrowth(-Number.MAX_VALUE, Number.MAX_VALUE)).toBe(-2); expect(formatFinancialCell("1", Number.POSITIVE_INFINITY).growthText.trim()).toBe(""); }); test("uses annual rows with a TTM column when quarterly data is available", () => { const table = buildFinancialTableModel(createFinancials(), { period: "annual", statement: "income", }); expect(table?.period).toBe("annual"); expect(table?.statements.map((statement) => statement.date)).toEqual(["TTM", "2025-12-31", "2024-12-31"]); const revenueRow = table?.rows.find((row) => row.summaryKey === "totalRevenue"); expect(revenueRow?.cells.map((cell) => cell.value)).toEqual([100, 150, 100]); }); // REF: SEC fiscal year-end balance points sit among the quarterly rows. test("omits periods with nothing on the selected statement", () => { const financials = createFinancials(); financials.quarterlyStatements.splice(3, 0, { date: "2025-10-04", totalAssets: 900, totalEquity: 400, dateSource: "sec" }); const dates = (statement: string) => buildFinancialTableModel(financials, { period: "quarterly", statement, quarterlyLimit: 4 }) ?.statements.map((row) => row.date); expect(dates("income")).toEqual(["2025-12-31", "2025-09-30", "2025-06-30", "2025-03-31"]); expect(dates("balance")).toEqual(["2025-10-04"]); }); test("hides lines that repeat their group's headline in every shown period", () => { const labels = (netIncomeCommonStockholders: number) => buildFinancialTableModel({ annualStatements: [ { date: "2024-12-31", totalRevenue: 100, operatingRevenue: 100, netIncome: 25, netIncomeCommonStockholders: 25, eps: 1 }, { date: "2025-12-31", totalRevenue: 150, operatingRevenue: 150, netIncome: 45, netIncomeCommonStockholders, eps: 2 }, ], quarterlyStatements: [], priceHistory: [], }, { period: "annual", statement: "income", expandAll: true })?.rows.map((row) => row.unitLabel) ?? []; expect(labels(45)).not.toContain("Operating Revenue"); expect(labels(45)).not.toContain("Income Common"); expect(labels(44)).toContain("Income Common"); expect(labels(45)).toContain("Diluted EPS"); }); // MSFT cash flow: D&A and Depreciation, Buybacks and Stock Payments. test("hides a line that repeats a sibling above it in every shown period", () => { const labels = (depreciation: number) => buildFinancialTableModel({ annualStatements: [ { date: "2024-12-31", operatingCashFlow: 100, depreciationAndAmortization: 20, depreciation: 20, stockBasedCompensation: 5 }, { date: "2025-12-31", operatingCashFlow: 120, depreciationAndAmortization: 30, depreciation, stockBasedCompensation: 6 }, ], quarterlyStatements: [], priceHistory: [], }, { period: "annual", statement: "cashflow", expandAll: true })?.rows.map((row) => row.unitLabel) ?? []; expect(labels(30)).toContain("D&A"); expect(labels(30)).not.toContain("Depreciation"); expect(labels(25)).toContain("Depreciation"); }); test("applies financial row semantics to growth color values", () => { const table = buildFinancialTableModel({ financialCurrency: "USD", annualStatements: [ { date: "2024-12-31", totalRevenue: 100, costOfRevenue: 50, otherIncomeExpense: 1, basicShares: 10, }, { date: "2025-12-31", totalRevenue: 120, costOfRevenue: 60, otherIncomeExpense: 2, basicShares: 9, }, ], quarterlyStatements: [], priceHistory: [], }, { period: "annual", statement: "income", expandAll: true, }); const revenueRow = table?.rows.find((row) => row.summaryKey === "totalRevenue"); const costRow = table?.rows.find((row) => row.key === "costOfRevenue"); const otherIncomeRow = table?.rows.find((row) => row.key === "otherIncomeExpense"); const sharesRow = table?.rows.find((row) => row.key === "basicShares"); expect(revenueRow?.cells[0]?.semanticGrowth).toBeCloseTo(0.2); expect(costRow?.cells[0]?.growth).toBeCloseTo(0.2); expect(costRow?.cells[0]?.semanticGrowth).toBeCloseTo(-0.2); expect(otherIncomeRow?.cells[0]?.growth).toBeCloseTo(1); expect(otherIncomeRow?.cells[0]?.semanticGrowth).toBe(0); expect(sharesRow?.cells[0]?.growth).toBeCloseTo(-0.1); expect(sharesRow?.cells[0]?.semanticGrowth).toBeCloseTo(0.1); }); test("normalizes shorthand period and statement options", () => { expect(resolveFinancialPeriodOption("qtr")).toBe("quarterly"); expect(resolveFinancialPeriodOption("fy")).toBe("annual"); expect(resolveFinancialSubTabKey("balance sheet")).toBe("balance"); expect(resolveFinancialSubTabKey("cf")).toBe("cashflow"); }); }); test("TTM keeps opening/closing cash and averages period shares instead of summing balances", () => { const quarters = ["2025-03-31", "2025-06-30", "2025-09-30", "2025-12-31"].map((date, index) => ({ date, currency: "USD", totalRevenue: 10, beginningCashPosition: 100 + index * 10, endCashPosition: 110 + index * 10, changesInCash: 10, totalAssets: 200 + index, basicShares: 100 - index * 10, dilutedShares: 110 - index * 10, })); const ttm = computeTTM([...quarters].reverse()); expect(ttm).toMatchObject({ currency: "USD", totalRevenue: 40, beginningCashPosition: 100, endCashPosition: 140, changesInCash: 40, totalAssets: 203, basicShares: 85, dilutedShares: 95 }); }); test("TTM rejects missing quarters, semiannual reports, and incompatible or partially known currencies", () => { const dates = ["2024-06-30", "2024-12-31", "2025-06-30", "2025-12-31"]; expect(computeTTM(dates.map((date) => ({ date, totalRevenue: 100 })))).toBeNull(); expect(computeTTM(["2025-03-31", "2025-06-30", "2025-09-30", "2025-12-31"].map((date, index) => ({ date, totalRevenue: 100, currency: index === 0 ? "USD" : undefined, })))).toBeNull(); expect(computeTTM(["2024-09-30", "2025-03-31", "2025-06-30", "2025-09-30"].map((date) => ({ date, totalRevenue: 100 })))).toBeNull(); expect(computeTTM(["2025-03-31", "2025-06-30", "2025-09-30", "2025-12-31"].map((date, index) => ({ date, totalRevenue: 100, currency: index === 0 ? "EUR" : "USD", })))).toBeNull(); }); test("statement reporting currency stays separate from converted summary fundamentals", () => { const summary = { financialCurrency: "TWD", fundamentals: { financialCurrency: "USD" } }; expect(financialStatementCurrency(summary, [{ date: "2025-12-31", totalRevenue: 3.8e12 }])).toBe("TWD"); expect(financialStatementCurrency({}, [{ date: "2025-12-31", currency: "TWD" }])).toBe("TWD"); expect(financialStatementCurrency({ fundamentals: { financialCurrency: "USD" } }, [{ date: "2025-12-31" }])).toBeUndefined(); }); test("growth is unavailable across missing years or a reporting currency change", () => { const table = buildFinancialTableModel({ annualStatements: [ { date: "2022-12-31", currency: "USD", totalRevenue: 100 }, { date: "2024-12-31", currency: "USD", totalRevenue: 200 }, { date: "2025-12-31", currency: "EUR", totalRevenue: 180 }, ], quarterlyStatements: [] }); expect(table?.rows.find((row) => row.summaryKey === "totalRevenue")?.cells.map((cell) => cell.growth)).toEqual([undefined, undefined, undefined]); }); test("annual growth finds a unique prior year around interleaved ADR EPS observations", () => { const annualStatements = [ { date: "2025-03-31", currency: "JPY", totalRevenue: 48_036_704_000_000, eps: 3595.6 }, { date: "2025-06-30", currency: "JPY", eps: 3251.3 }, { date: "2025-09-30", currency: "JPY", eps: 3534.8 }, { date: "2026-03-31", currency: "JPY", totalRevenue: 50_684_952_000_000, eps: 2952.5 }, ]; const model = () => buildFinancialTableModel({ annualStatements, quarterlyStatements: [] }, { period: "annual", statement: "income", expandAll: true, })!; const revenue = (table: ReturnType) => table.rows.find((row) => row.summaryKey === "totalRevenue")!.cells[0]!; const table = model(); expect(revenue(table)).toMatchObject({ value: 50_684_952_000_000 }); expect(revenue(table).growth).toBeCloseTo(0.0551296775); expect(table.rows.find((row) => row.key === "eps")!.cells[0]!.growth).toBeCloseTo(-0.1788574925); expect(table.statements.map((row) => row.date)).toEqual([...annualStatements].reverse().map((row) => row.date)); annualStatements[0]!.currency = "USD"; expect(revenue(model()).growth).toBeUndefined(); annualStatements[0]!.currency = "JPY"; annualStatements.splice(1, 0, { date: "2025-04-30", currency: "JPY", totalRevenue: 49_000_000_000_000, eps: 3500 }); expect(revenue(model()).growth).toBeUndefined(); }); test("annual balance sheets show the latest dated snapshot with comparable year-on-year growth", () => { const financials = { annualStatements: [{ date: "2025-12-31", currency: "USD", totalAssets: 200 }], quarterlyStatements: [ { date: "2025-06-30", currency: "USD", totalAssets: 160 }, { date: "2026-06-30", currency: "USD", totalAssets: 240 }, ], }; const table = buildFinancialTableModel(financials, { period: "annual", statement: "balance" })!; expect(table.statements.map(({ date }) => date)).toEqual(["2026-06-30", "2025-12-31"]); expect(table.rows.find(({ summaryKey }) => summaryKey === "totalAssets")?.cells[0]).toMatchObject({ value: 240, growth: 0.5 }); financials.quarterlyStatements[0]!.currency = "EUR"; const changedCurrency = buildFinancialTableModel(financials, { period: "annual", statement: "balance" })!; expect(changedCurrency.rows.find(({ summaryKey }) => summaryKey === "totalAssets")?.cells[0]?.growth).toBeUndefined(); financials.quarterlyStatements = [{ date: "2025-12-31", currency: "USD", totalAssets: 200 }]; expect(buildFinancialTableModel(financials, { period: "annual", statement: "balance" })?.statements.map(({ date }) => date)).toEqual(["2025-12-31"]); }); test("an EPS-only quarter cannot replace the latest available balance sheet", () => { // Reduced from the public BAC capture: June coverage has EPS but no balance sheet. const financials: TickerFinancials = { annualStatements: [{ date: "2025-12-31", currency: "USD", totalAssets: 3_411_738_000_000 }], quarterlyStatements: [ { date: "2025-03-31", currency: "USD", totalAssets: 3_349_424_000_000 }, { date: "2026-03-31", currency: "USD", totalAssets: 3_496_186_000_000 }, { date: "2026-06-30", currency: "USD", eps: 1.21 }, ], priceHistory: [], }; const source = structuredClone(financials); const balance = buildFinancialTableModel(financials, { period: "annual", statement: "balance" })!; expect(balance.statements.map(({ date }) => date)).toEqual(["2026-03-31", "2025-12-31"]); const assets = balance.rows.find(({ summaryKey }) => summaryKey === "totalAssets")!.cells[0]!; expect(assets.value).toBe(3_496_186_000_000); expect(assets.growth).toBeCloseTo(3_496_186_000_000 / 3_349_424_000_000 - 1); const income = buildFinancialTableModel(financials, { period: "quarterly", statement: "income", expandAll: true })!; expect(income.statements[0]!.date).toBe("2026-06-30"); expect(income.rows.find(({ key }) => key === "eps")!.cells[0]!.value).toBe(1.21); expect(financials).toEqual(source); // Missing/non-finite fields are not balance coverage, but a reported zero is. financials.quarterlyStatements.at(-1)!.totalAssets = NaN; expect(buildFinancialTableModel(financials, { period: "annual", statement: "balance" })!.statements[0]!.date).toBe("2026-03-31"); financials.quarterlyStatements.at(-1)!.totalDebt = 0; const zero = buildFinancialTableModel(financials, { period: "annual", statement: "balance", expandAll: true })!; expect(zero.statements[0]!.date).toBe("2026-06-30"); expect(zero.rows.find(({ key }) => key === "totalDebt")!.cells[0]!.value).toBe(0); }); test("liabilities plus equity requires its own components and cannot copy total assets", () => { const table = buildFinancialTableModel({ annualStatements: [ { date: "2022-12-31", totalAssets: 100, totalLiabilities: 60, totalEquityGrossMinorityInterest: 35 }, { date: "2023-12-31", totalAssets: 110, totalLiabilities: 70, totalEquity: 40 }, { date: "2024-12-31", totalAssets: 120, totalLiabilities: 80, totalEquityGrossMinorityInterest: 0 }, { date: "2025-12-31", totalAssets: 130 }, ], quarterlyStatements: [], }, { period: "annual", statement: "balance" })!; const total = table.rows.find(({ id }) => id === "balance:liabilities-equity:0")!; expect(total.cells.map(({ value }) => value)).toEqual([undefined, 80, undefined, 95]); expect(total.cells[0]!.valueText.trim()).toBe("—"); expect(table.rows.find(({ summaryKey }) => summaryKey === "totalAssets")!.cells.map(({ value }) => value)).toEqual([130, 120, 110, 100]); });