"""Vendor 360 (cross-channel) + CFO leadership dashboard composition.""" from __future__ import annotations from decimal import Decimal import pytest from app.domains.finance import read_service from app.domains.finance.models import FinanceSpendFact PERIOD = "2026-05" def _fact(vendor, cost_type, usd, *, dept="研发", basis="consumption", period=PERIOD): return FinanceSpendFact( fact_key=f"k|{vendor}|{cost_type}|{usd}|{dept}|{period}", period=period, cost_type=cost_type, basis=basis, amount_native=Decimal(str(usd)), currency_native="USD", amount_usd=Decimal(str(usd)), fx_basis="native_usd", allocation_source="rule", vendor=vendor, department=dept, source_domain=cost_type, rollup_fingerprint="t", is_provisional=False, ) @pytest.mark.asyncio async def test_vendor_360_unifies_consumption_excluding_other_bases(db_session): db = db_session # AWS spans multiple consumption cost types; a procurement-basis PO for # the SAME vendor must NOT be summed in — procurement and consumption are # two lenses on one spend, and summing them double-counts the money. db.add(_fact("AWS", "cloud", 3000)) db.add(_fact("AWS", "procurement", 2000, basis="procurement")) # excluded db.add(_fact("AWS", "cloud", 500, dept="销售")) db.add(_fact("Microsoft", "software", 1000)) # other vendor await db.flush() res = await read_service.vendor_360(db, PERIOD, "AWS") assert res["total_usd"] == "3500.00" # 3000 + 500 (procurement leg excluded) by_ct = {r["cost_type"]: r["amount_usd"] for r in res["by_cost_type"]} assert by_ct["cloud"] == "3500.00" and "procurement" not in by_ct by_dep = {r["department"]: r["amount_usd"] for r in res["by_department"]} assert by_dep["研发"] == "3000.00" and by_dep["销售"] == "500.00" # Concentration = AWS consumption / company consumption (3500 + 1000). assert res["concentration_pct"] == round(3500 / 4500 * 100, 2) assert len(res["trend"]) == 12 and res["trend"][-1]["period"] == PERIOD @pytest.mark.asyncio async def test_top_vendors_ranked(db_session): db = db_session db.add(_fact("AWS", "cloud", 3000)) db.add(_fact("Microsoft", "software", 5000)) db.add(_fact(None, "cloud", 9999)) # null vendor → excluded # A large procurement PO for AWS must NOT inflate its consumption rank. db.add(_fact("AWS", "procurement", 9000, basis="procurement")) await db.flush() res = await read_service.top_vendors(db, PERIOD, limit=5) vendors = [v["vendor"] for v in res["vendors"]] assert vendors[0] == "Microsoft" and "AWS" in vendors # 5000 > 3000 assert all(v["vendor"] is not None for v in res["vendors"]) aws = next(v for v in res["vendors"] if v["vendor"] == "AWS") assert aws["amount_usd"] == "3000.00" # procurement leg excluded @pytest.mark.asyncio async def test_leadership_dashboard_composes(db_session): db = db_session db.add(_fact("AWS", "cloud", 3000)) db.add(_fact("Microsoft", "software", 1000)) await db.flush() res = await read_service.leadership_dashboard(db, PERIOD) # Bundled, consistent snapshot — all sections present without error. assert res["period"] == PERIOD assert set(res) >= { "summary", "headcount", "cost_per_headcount_usd", "dept_movers", "top_vendors", "bu_costs", "budget_health", } assert res["summary"]["consumption_total_usd"] == "4000.00" assert any(v["vendor"] == "AWS" for v in res["top_vendors"])