"""Tests for dashboard/financials.py — fiscal calendar robustness. Each company has a different fiscal year end month: AMD — December (fy_end_month=12) MSFT — June (fy_end_month=6) AAPL — September (fy_end_month=9) ← what the old hardcoded table assumed NVDA — January (fy_end_month=1) """ from __future__ import annotations from unittest.mock import patch import pytest # --------------------------------------------------------------------------- # _quarter_label — pure-function tests (no Streamlit, no network) # --------------------------------------------------------------------------- class TestQuarterLabelAMD: """AMD fiscal year ends December.""" def test_q1_fy24(self): from dashboard.financials import _quarter_label assert _quarter_label("2024-03-31", fy_end_month=12) == "Q1 FY24 · MAR" def test_q2_fy24(self): from dashboard.financials import _quarter_label assert _quarter_label("2024-06-30", fy_end_month=12) == "Q2 FY24 · JUN" def test_q3_fy24(self): from dashboard.financials import _quarter_label assert _quarter_label("2024-09-30", fy_end_month=12) == "Q3 FY24 · SEP" def test_q4_fy24(self): from dashboard.financials import _quarter_label assert _quarter_label("2024-12-31", fy_end_month=12) == "Q4 FY24 · DEC" class TestQuarterLabelMSFT: """MSFT fiscal year ends June.""" def test_q1_fy25(self): from dashboard.financials import _quarter_label assert _quarter_label("2024-09-30", fy_end_month=6) == "Q1 FY25 · SEP" def test_q2_fy25(self): from dashboard.financials import _quarter_label assert _quarter_label("2024-12-31", fy_end_month=6) == "Q2 FY25 · DEC" def test_q3_fy25(self): from dashboard.financials import _quarter_label assert _quarter_label("2025-03-31", fy_end_month=6) == "Q3 FY25 · MAR" def test_q4_fy25(self): from dashboard.financials import _quarter_label assert _quarter_label("2025-06-30", fy_end_month=6) == "Q4 FY25 · JUN" class TestQuarterLabelAAPL: """AAPL fiscal year ends September — must match existing correct behavior.""" def test_q1_fy25(self): from dashboard.financials import _quarter_label assert _quarter_label("2024-12-31", fy_end_month=9) == "Q1 FY25 · DEC" def test_q2_fy25(self): from dashboard.financials import _quarter_label assert _quarter_label("2025-03-31", fy_end_month=9) == "Q2 FY25 · MAR" def test_q3_fy25(self): from dashboard.financials import _quarter_label assert _quarter_label("2025-06-30", fy_end_month=9) == "Q3 FY25 · JUN" def test_q4_fy25(self): from dashboard.financials import _quarter_label assert _quarter_label("2025-09-30", fy_end_month=9) == "Q4 FY25 · SEP" class TestQuarterLabelNVDA: """NVDA fiscal year ends January.""" def test_q1_fy25(self): from dashboard.financials import _quarter_label assert _quarter_label("2024-04-30", fy_end_month=1) == "Q1 FY25 · APR" def test_q2_fy25(self): from dashboard.financials import _quarter_label assert _quarter_label("2024-07-31", fy_end_month=1) == "Q2 FY25 · JUL" def test_q3_fy25(self): from dashboard.financials import _quarter_label assert _quarter_label("2024-10-31", fy_end_month=1) == "Q3 FY25 · OCT" def test_q4_fy25(self): from dashboard.financials import _quarter_label assert _quarter_label("2025-01-31", fy_end_month=1) == "Q4 FY25 · JAN" class TestQuarterLabelEdgeCases: def test_invalid_date_returns_raw_string(self): """Bad date string must not crash — returns original string.""" from dashboard.financials import _quarter_label assert _quarter_label("not-a-date", fy_end_month=12) == "not-a-date" def test_none_fy_end_month_returns_raw_date(self): """When fy_end_month is None (unknown), return the date string unchanged.""" from dashboard.financials import _quarter_label result = _quarter_label("2024-06-30", fy_end_month=None) assert result == "2024-06-30" class TestFinancialDataQuality: @staticmethod def _row(period, filing_date, form_type, status="VERIFIED", warnings=None): return { "period": period, "filing_date": filing_date, "form_type": form_type, "period_basis": "annual" if form_type == "10-K" else "quarter", "revenue": 10_000_000_000.0, "revenue_yoy_pct": 12.5, "eps": 2.5, "gross_margin": 0.6, "operating_margin": 0.4, "free_cash_flow": 3_000_000_000.0, "data_quality_status": status, "quality_warnings": warnings or [], } def test_display_rows_masks_check_required_fields_and_hides_legacy(self): from dashboard.financials import _display_rows rows = [ self._row( "Q22025", "2025-08-01", "10-Q", "CHECK_REQUIRED", ["free_cash_flow:fallback_non_sec"], ), self._row("Q12025", "2025-05-01", "10-Q"), self._row("FY2024", "2024-11-01", "10-K", "LEGACY_UNVERIFIED"), ] display_rows = _display_rows(rows) assert [row["period"] for row in display_rows] == ["Q22025", "Q12025"] partial = display_rows[0] assert partial["free_cash_flow"] is None assert partial["revenue"] == 10_000_000_000.0 assert partial["eps"] == 2.5 assert partial["_masked_fields"] == ["free_cash_flow"] def test_display_row_masks_revenue_and_derived_yoy(self): from dashboard.financials import _display_row row = self._row( "Q22025", "2025-08-01", "10-Q", "CHECK_REQUIRED", ["revenue:fallback_non_sec"], ) displayed = _display_row(row) assert displayed is not None assert displayed["revenue"] is None assert displayed["revenue_yoy_pct"] is None assert displayed["eps"] == 2.5 assert displayed["_masked_fields"] == ["revenue", "revenue_yoy_pct"] def test_display_row_hides_legacy_unverified(self): from dashboard.financials import _display_row legacy = self._row( "FY2024", "2024-11-01", "10-K", "LEGACY_UNVERIFIED" ) assert _display_row(legacy) is None def test_comparable_prior_does_not_compare_annual_with_quarter(self): from dashboard.financials import _comparable_prior latest = self._row("FY2025", "2025-11-01", "10-K") recent_quarter = self._row("Q32025", "2025-08-01", "10-Q") prior_annual = self._row("FY2024", "2024-11-01", "10-K") assert _comparable_prior( [latest, recent_quarter, prior_annual], latest )["period"] == "FY2024" def test_comparable_prior_accepts_sanitized_check_required_lineage(self): from dashboard.financials import _comparable_prior, _display_rows latest = self._row("Q22025", "2025-08-01", "10-Q") partial_prior = self._row( "Q12025", "2025-05-01", "10-Q", "CHECK_REQUIRED", ["free_cash_flow:fallback_non_sec"], ) rows = _display_rows([latest, partial_prior]) prior = _comparable_prior(rows, rows[0]) assert prior["period"] == "Q12025" assert prior["free_cash_flow"] is None assert prior["revenue"] == 10_000_000_000.0 def test_comparable_prior_requires_exact_adjacent_fiscal_period(self): from dashboard.financials import _comparable_prior latest = self._row("Q32025", "2025-11-01", "10-Q") non_adjacent = self._row("Q12025", "2025-05-01", "10-Q") assert _comparable_prior([latest, non_adjacent], latest) == {} def test_masked_message_only_receives_fully_hidden_rows(self): from dashboard.financials import _display_row, _masked_quality_message partial = self._row( "Q22025", "2025-08-01", "10-Q", "CHECK_REQUIRED", ["revenue:duration_mismatch"], ) legacy = self._row( "FY2024", "2024-11-01", "10-K", "LEGACY_UNVERIFIED" ) masked_rows = [row for row in [partial, legacy] if _display_row(row) is None] message = _masked_quality_message(masked_rows) assert "FY2024: LEGACY_UNVERIFIED" in message assert "Q22025" not in message assert "revenue:duration_mismatch" not in message assert "no citable SEC lineage" in message assert "not VERIFIED" not in message @patch("dashboard.financials.st.plotly_chart") def test_chart_margins_preserves_masked_gross_margin_as_none( self, mock_plotly_chart ): from dashboard.financials import _chart_margins, _display_row row = self._row( "Q22025", "2025-08-01", "10-Q", "CHECK_REQUIRED", ["gross_margin:fallback_non_sec"], ) displayed = _display_row(row) assert displayed is not None assert displayed["gross_margin"] is None assert displayed["revenue"] == 10_000_000_000.0 assert displayed["eps"] == 2.5 _chart_margins(["Q2 FY25"], [displayed]) margins_figure = mock_plotly_chart.call_args_list[0].args[0] assert list(margins_figure.data[0].y) == [None] @patch("dashboard.financials.st.plotly_chart") def test_chart_cash_preserves_masked_free_cash_flow_as_none( self, mock_plotly_chart ): from dashboard.financials import _chart_cash, _display_row row = self._row( "Q22025", "2025-08-01", "10-Q", "CHECK_REQUIRED", ["free_cash_flow:fallback_non_sec"], ) displayed = _display_row(row) assert displayed is not None assert displayed["free_cash_flow"] is None assert displayed["revenue"] == 10_000_000_000.0 assert displayed["eps"] == 2.5 _chart_cash(["Q2 FY25"], [displayed]) cash_figure = mock_plotly_chart.call_args_list[0].args[0] assert list(cash_figure.data[0].y) == [None]