"""Fiscal-year arithmetic — the April-start window the org actually uses. Covers the pure helpers in :mod:`app.core.fiscal_year` and an integration check that ``budget_vs_actual`` measures a fiscal year against its Apr→Mar window (including the Jan–Mar months that fall in the NEXT calendar year) and excludes the prior fiscal year's spend. """ from __future__ import annotations from datetime import UTC, datetime from decimal import Decimal import pytest from app.core import fiscal_year as fy from app.domains.finance import budget_service # ─── Pure helpers (no DB) ─────────────────────────────────────── def test_fy_bounds_april_and_january(): assert fy.fy_bounds(2026, 4) == ("2026-04", "2027-03") assert fy.fy_bounds(2026, 1) == ("2026-01", "2026-12") assert fy.fy_bounds(2026, 12) == ("2026-12", "2027-11") def test_periods_in_fy_april(): periods = fy.periods_in_fy(2026, 4) assert len(periods) == 12 assert periods[0] == "2026-04" assert periods[8] == "2026-12" assert periods[9] == "2027-01" assert periods[-1] == "2027-03" def test_fy_of_period_rolls_back_before_start_month(): # April start: Apr–Dec 2026 → FY2026; Jan–Mar 2027 → still FY2026. assert fy.fy_of_period("2026-04", 4) == 2026 assert fy.fy_of_period("2026-12", 4) == 2026 assert fy.fy_of_period("2027-01", 4) == 2026 assert fy.fy_of_period("2027-03", 4) == 2026 assert fy.fy_of_period("2027-04", 4) == 2027 # Months before April roll into the prior FY. assert fy.fy_of_period("2026-03", 4) == 2025 # January start = calendar year. assert fy.fy_of_period("2026-07", 1) == 2026 def test_current_fiscal_year_in_q1_belongs_to_prior_fy(): # February 2027 is still inside FY2026 under an April start. feb = datetime(2027, 2, 15, tzinfo=UTC) assert fy.current_fiscal_year(4, feb) == 2026 may = datetime(2026, 5, 1, tzinfo=UTC) assert fy.current_fiscal_year(4, may) == 2026 def test_months_elapsed_april(): assert fy.months_elapsed(2026, "2026-04", 4) == 1 assert fy.months_elapsed(2026, "2026-06", 4) == 3 assert fy.months_elapsed(2026, "2026-12", 4) == 9 assert fy.months_elapsed(2026, "2027-01", 4) == 10 assert fy.months_elapsed(2026, "2027-03", 4) == 12 # Before the FY starts → 0; after it ends → clamped to 12. assert fy.months_elapsed(2026, "2026-03", 4) == 0 assert fy.months_elapsed(2026, "2027-06", 4) == 12 def test_default_as_of_period_clamps_to_fy_window(): # Past FY → its last month (fully elapsed). assert fy.default_as_of_period(2020, 4, datetime(2026, 6, 1, tzinfo=UTC)) == "2021-03" # Current FY → current month (forecast stays meaningful mid-year). assert fy.default_as_of_period(2026, 4, datetime(2026, 6, 1, tzinfo=UTC)) == "2026-06" # Current month in Q1 of the next calendar year, still the same FY. assert fy.default_as_of_period(2026, 4, datetime(2027, 2, 1, tzinfo=UTC)) == "2027-02" # Future FY → current month (→ 0 elapsed). assert fy.default_as_of_period(2030, 4, datetime(2026, 6, 1, tzinfo=UTC)) == "2026-06" def test_fy_label_is_named_after_the_year_the_fy_ends_in(): """FY27 = the window that ENDS in 2027 (2026-04 ~ 2027-03). Changed 2026-08-24: the platform used to render this as "FY2026", named after the start year, which is not what anyone in the company calls it. The stored integer is still the start year — only the label moved, so no money was re-bucketed. """ assert fy.fy_label(2026, 4) == "FY27 (2026-04 ~ 2027-03)" assert fy.fy_label(2025, 4) == "FY26 (2025-04 ~ 2026-03)" assert fy.fy_short_label(2026, 4) == "FY27" # A January start has start year == end year, so it does not shift. assert fy.fy_label(2026, 1) == "FY26 (2026)" assert fy.fy_short_label(2026, 1) == "FY26" # Century roll-over stays two digits rather than becoming "FY100". assert fy.fy_short_label(2099, 4) == "FY00" def test_coerce_start_month_is_lenient(): assert fy._coerce_start_month(4) == 4 assert fy._coerce_start_month("7") == 7 assert fy._coerce_start_month(0) == 4 # out of range → default assert fy._coerce_start_month(13) == 4 assert fy._coerce_start_month("nope") == 4 assert fy._coerce_start_month(None) == 4 # ─── Setting resolution (DB) ──────────────────────────────────── @pytest.mark.asyncio async def test_get_start_month_default_then_setting(db_session): db = db_session # Absent → default April. assert await fy.get_fiscal_year_start_month(db) == 4 from app.integrations.models import PlatformSetting await db.merge(PlatformSetting(key=fy.PLATFORM_SETTING_FY_START_MONTH, value=1)) await db.flush() assert await fy.get_fiscal_year_start_month(db) == 1 # Malformed value falls back to default, never raises. await db.merge(PlatformSetting(key=fy.PLATFORM_SETTING_FY_START_MONTH, value="bad")) await db.flush() assert await fy.get_fiscal_year_start_month(db) == 4 # ─── Integration: budget measured against the Apr→Mar window ──── def _fact(period: str, usd: str, *, cost_type="cloud", dept="Eng", cc="C1"): from app.domains.finance.models import FinanceSpendFact return FinanceSpendFact( fact_key=f"f-{period}-{cost_type}-{cc}-{usd}", period=period, cost_type=cost_type, basis="consumption", sub_category="x", vendor="aws", department=dept, cost_center=cc, allocation_source="scope_owner", amount_native=Decimal(usd), currency_native="USD", amount_usd=Decimal(usd), fx_basis="native_usd", is_provisional=True, source_domain="cmdb", rollup_fingerprint="x", ) @pytest.mark.asyncio async def test_budget_vs_actual_spans_calendar_boundary_april_fy(db_session): db = db_session await budget_service.create_budget(db, fiscal_year=2026, amount_usd=Decimal("1200")) # Prior FY (March 2026 is FY2025) — MUST be excluded. db.add(_fact("2026-03", "999")) # FY2026 window: Apr 2026 … Mar 2027 — including Jan/Feb 2027. db.add(_fact("2026-04", "100")) db.add(_fact("2026-05", "100")) db.add(_fact("2027-01", "100")) db.add(_fact("2027-02", "100")) # Future month of the FY (after as_of) — counted into annual but not YTD. db.add(_fact("2027-03", "100")) await db.flush() # Default start_month resolves from the (unseeded) setting → April. rep = await budget_service.budget_vs_actual(db, 2026, "2027-02") assert rep["fiscal_year_start_month"] == 4 assert rep["fy_period_start"] == "2026-04" assert rep["fy_period_end"] == "2027-03" assert rep["months_elapsed"] == 11 # Apr 2026 → Feb 2027 inclusive item = rep["items"][0] # YTD = the four months on/before 2027-02 within the window; the $999 in # 2026-03 (prior FY) is excluded. assert item["ytd_actual_usd"] == "400.00"