"""BU fiscal-year cost view — per-一级部门 four-axis spend vs budget, with the hardware depreciation/cash basis toggle and multi-level drill. Fiscal year defaults to April start, so FY2026 = 2026-04 … 2027-03; an explicit ``as_of_period`` keeps the assertions independent of "now". """ from __future__ import annotations import uuid from decimal import Decimal import pytest import pytest_asyncio from app.domains.finance import budget_service, read_service from app.domains.finance.models import FinanceSpendFact from app.identity.person_models import IdentityOrgMembership, IdentityPerson FY = 2026 AS_OF = "2026-05" # 2 months into FY2026 (Apr, May) async def _person_in_bu(db, bu: str) -> uuid.UUID: pid = uuid.uuid4() db.add(IdentityPerson( id=pid, work_email=f"{pid.hex[:8]}@t.com", display_name=f"{bu} Person", employment_status="active", source_system="successfactors", hris_person_id=f"sf-{pid.hex[:8]}", )) db.add(IdentityOrgMembership( id=uuid.uuid4(), person_id=pid, org_node_source_id=f"DP-{pid.hex[:6]}", org_node_type="department", org_node_name=bu, org_path=f"PingCAP / {bu}", is_primary=True, )) await db.flush() return pid def _fact(period, cost_type, usd, *, basis="consumption", owner=None, dept=None, vendor="v", sub="s"): return FinanceSpendFact( fact_key=f"f-{uuid.uuid4().hex}", period=period, cost_type=cost_type, basis=basis, sub_category=sub, vendor=vendor, department=dept, owner_person_id=(str(owner) if owner else None), allocation_source=("derived" if owner else "none"), amount_native=Decimal(usd), currency_native="USD", amount_usd=Decimal(usd), fx_basis="native_usd", is_provisional=True, source_domain="x", rollup_fingerprint="x", ) @pytest_asyncio.fixture async def seeded(db_session): db = db_session eng = await _person_in_bu(db, "Eng") sales = await _person_in_bu(db, "Sales") db.add_all([ _fact("2026-04", "cloud", "100", owner=eng, dept="Eng"), _fact("2026-05", "software", "50", owner=eng, dept="Eng"), _fact("2026-04", "hardware", "30", owner=eng, dept="Eng"), # depreciation _fact("2026-04", "hardware", "1000", basis="cash", owner=eng, dept="Eng"), _fact("2026-05", "ai", "40", owner=sales, dept="Sales"), _fact("2026-04", "cloud", "20", owner=None, dept=None), # unallocated # Outside the FY window (FY2025) — must be excluded. _fact("2026-03", "cloud", "999", owner=eng, dept="Eng"), ]) # Eng BU budget for the fiscal year. await budget_service.create_budget(db, fiscal_year=FY, amount_usd=Decimal("1200"), department="Eng") await db.flush() return db @pytest.mark.asyncio async def test_bu_costs_depreciation_default(seeded): db = seeded rep = await read_service.bu_fiscal_year_costs(db, FY, as_of_period=AS_OF) assert rep["fiscal_year_start_month"] == 4 assert rep["fy_period_start"] == "2026-04" assert rep["months_elapsed"] == 2 assert rep["hardware_basis"] == "depreciation" by_bu = {i["bu"]: i for i in rep["items"]} eng = by_bu["Eng"] # cloud 100 + software 50 + hardware(dep) 30 = 180; FY2025 999 excluded. assert eng["ytd_actual_usd"] == "180.00" assert eng["by_axis"]["hardware"] == "30.00" assert eng["by_axis"]["cloud"] == "100.00" assert eng["annual_budget_usd"] == "1200.00" # forecast = 180 / 2 * 12 = 1080. assert eng["forecast_year_end_usd"] == "1080.00" assert by_bu["Sales"]["ytd_actual_usd"] == "40.00" assert by_bu["__unallocated__"]["ytd_actual_usd"] == "20.00" @pytest.mark.asyncio async def test_bu_costs_cash_basis_swaps_hardware(seeded): db = seeded rep = await read_service.bu_fiscal_year_costs(db, FY, as_of_period=AS_OF, hardware_basis="cash") eng = {i["bu"]: i for i in rep["items"]}["Eng"] # hardware switches to the cash row (1000), depreciation (30) excluded. assert eng["by_axis"]["hardware"] == "1000.00" assert eng["ytd_actual_usd"] == "1150.00" # 100 + 50 + 1000 @pytest.mark.asyncio async def test_company_wide_budget_kept_in_total_not_per_bu(db_session): db = db_session # A company-wide (no-department) budget can't be attributed to one BU, # but must still count in the company total so it doesn't understate. # It is the only budget in the FY: under the wildcard non-overlap # contract it cannot coexist with a narrower pool. await budget_service.create_budget(db, fiscal_year=FY, amount_usd=Decimal("5000")) await db.flush() rep = await read_service.bu_fiscal_year_costs(db, FY, as_of_period=AS_OF) assert rep["company_wide_budget_usd"] == "5000.00" assert rep["total_annual_budget_usd"] == "5000.00" # The company-wide budget did NOT create a phantom BU row. assert all(i["bu"] != "__unallocated__" or Decimal(i["annual_budget_usd"]) == 0 for i in rep["items"]) @pytest.mark.asyncio async def test_bu_drill_by_owner_and_cost_type(seeded): db = seeded drill = await read_service.bu_cost_drill(db, FY, "Eng", group_by="owner", as_of_period=AS_OF) assert drill["total_usd"] == "180.00" assert drill["rows"][0]["owner_name"] == "Eng Person" # Narrow to one axis. cloud = await read_service.bu_cost_drill(db, FY, "Eng", group_by="vendor", cost_type="cloud", as_of_period=AS_OF) assert cloud["total_usd"] == "100.00"