"""Finance cost-governance & vendor/contract lens (PR3). Covers: * savings_opportunities — wasted (leaver/off-paid/inactive) + idle paid seats estimated to monthly USD at the live FX rate * contract_renewals — active contracts expiring within the horizon * contract_vs_actual — contracted amount vs linked PO actual spend """ from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta from decimal import Decimal import pytest import pytest_asyncio from app.core.coordinators import finance_governance_coordinator as gov from app.domains.asset.models import ( AssetSoftwareCatalog, SoftwareExternalHolder, SoftwareLicensePlan, SoftwareSeatAllocation, ) from app.domains.procurement.models import ( ProcurementContract, ProcurementOrder, ProcurementSupplier, ) from app.identity.person_models import IdentityPerson from app.integrations.models import Integration, PlatformSetting # Test FX: 1 CNY = 0.10 USD. RATE = 0.10 @pytest_asyncio.fixture async def seeded(db_session): db = db_session # Live CNY/USD rate (fresh updated_at via server_default). db.add(PlatformSetting(key="ai_spending.cny_to_usd_rate", value=RATE)) integ = Integration(id=uuid.uuid4(), kind="saas", provider="figma", name="Figma", slug="figma") cat = AssetSoftwareCatalog(id=uuid.uuid4(), name="Figma", category="design") db.add_all([integ, cat]) await db.flush() plan = SoftwareLicensePlan( id=uuid.uuid4(), software_id=cat.id, name="Figma Pro", price=Decimal("100"), billing_cycle="monthly", currency="CNY", ) db.add(plan) person = IdentityPerson(id=uuid.uuid4(), hris_person_id="P9", work_email="idle@x.com", display_name="Idle User", employment_status="active") db.add(person) await db.flush() # Two wasted seats → $10/mo each = $20. db.add(SoftwareExternalHolder( id=uuid.uuid4(), integration_id=integ.id, software_id=cat.id, plan_id=plan.id, provider="figma", email="leaver@x.com", category="leaver_residual", )) db.add(SoftwareExternalHolder( id=uuid.uuid4(), integration_id=integ.id, software_id=cat.id, plan_id=plan.id, provider="figma", email="ghost@x.com", category="off_paid_site", )) # One idle seat in the grace window → $10/mo. db.add(SoftwareSeatAllocation( id=uuid.uuid4(), plan_id=plan.id, user_id=person.id, status="active", allocated_at=datetime(2026, 1, 1, tzinfo=UTC), idle_warned_at=datetime(2026, 5, 1, tzinfo=UTC), )) # A contract expiring in 30 days + a linked completed PO. supplier = ProcurementSupplier(id=uuid.uuid4(), supplier_code="S1", name="Adobe", status="active") db.add(supplier) await db.flush() contract = ProcurementContract( id=uuid.uuid4(), contract_number="C-1", title="Adobe Master Agreement", supplier_id=supplier.id, status="active", total_amount=Decimal("1200"), currency="USD", expiry_date=(datetime.now(UTC).date() + timedelta(days=30)), ) db.add(contract) await db.flush() db.add(ProcurementOrder( id=uuid.uuid4(), order_number="PO-9", order_type="standard", status="completed", applicant_id=person.id, supplier_id=supplier.id, contract_id=contract.id, currency="CNY", total_amount=Decimal("700"), total_cny=Decimal("700"), completed_at=datetime.now(UTC), )) await db.flush() return db @pytest.mark.asyncio async def test_savings_wasted_and_idle(seeded): r = await gov.savings_opportunities(seeded) assert r["fx_available"] is True assert len(r["wasted_seats"]) == 2 assert len(r["idle_seats"]) == 1 assert Decimal(r["waste_monthly_usd"]) == Decimal("20.00") # 2 × 100 CNY × 0.1 assert Decimal(r["idle_monthly_usd"]) == Decimal("10.00") assert Decimal(r["total_monthly_usd"]) == Decimal("30.00") @pytest.mark.asyncio async def test_renewals_within_horizon(seeded): r = await gov.contract_renewals(seeded, within_days=90) assert len(r["renewals"]) == 1 row = r["renewals"][0] assert row["contract_number"] == "C-1" assert row["supplier"] == "Adobe" assert Decimal(row["contracted_usd"]) == Decimal("1200.00") assert 0 < row["days_left"] <= 30 @pytest.mark.asyncio async def test_renewals_excludes_far_future(seeded): r = await gov.contract_renewals(seeded, within_days=10) # contract is 30d out assert len(r["renewals"]) == 0 @pytest.mark.asyncio async def test_contract_vs_actual(seeded): r = await gov.contract_vs_actual(seeded) assert len(r["contracts"]) == 1 c = r["contracts"][0] assert Decimal(c["contracted_usd"]) == Decimal("1200.00") assert Decimal(c["actual_usd"]) == Decimal("70.00") # 700 CNY × 0.1 assert c["utilization_pct"] == pytest.approx(5.8, abs=0.1) @pytest.mark.asyncio async def test_savings_converts_third_currency_via_fx_row(db_session): """2026-06-17 fix: a plan in a third currency (not USD/CNY) must be converted to USD via a finance FX month row instead of being silently dropped from the headline savings total. """ from app.domains.finance.models import FinanceFxMonthly db = db_session period = datetime.now(UTC).strftime("%Y-%m") # 1 JPY = 0.0066 USD for the current month. db.add(FinanceFxMonthly(period=period, currency="JPY", usd_per_unit=Decimal("0.0066"), source="manual", frozen=False)) integ = Integration(id=uuid.uuid4(), kind="saas", provider="jpvendor", name="JP", slug="jpvendor") cat = AssetSoftwareCatalog(id=uuid.uuid4(), name="JP Tool", category="design") db.add_all([integ, cat]) await db.flush() plan = SoftwareLicensePlan( id=uuid.uuid4(), software_id=cat.id, name="JP Pro", price=Decimal("10000"), billing_cycle="monthly", currency="JPY", ) db.add(plan) await db.flush() db.add(SoftwareExternalHolder( id=uuid.uuid4(), integration_id=integ.id, software_id=cat.id, plan_id=plan.id, provider="jpvendor", email="jp@x.com", category="leaver_residual", )) await db.flush() r = await gov.savings_opportunities(db) # 10000 JPY/mo × 0.0066 = $66.00 — converted, not dropped. assert Decimal(r["waste_monthly_usd"]) == Decimal("66.00") assert r["unconverted_count"] == 0 assert r["fx_available"] is True @pytest.mark.asyncio async def test_savings_flags_unconvertible_currency(db_session): """A currency with no resolvable rate (no FX row, not CNY/USD) must NOT be booked at a 1.0 sentinel — the seat appears with monthly_usd=None and is counted as unconverted so the total is visibly understated.""" db = db_session integ = Integration(id=uuid.uuid4(), kind="saas", provider="euvendor", name="EU", slug="euvendor") cat = AssetSoftwareCatalog(id=uuid.uuid4(), name="EU Tool", category="design") db.add_all([integ, cat]) await db.flush() plan = SoftwareLicensePlan( id=uuid.uuid4(), software_id=cat.id, name="EU Pro", price=Decimal("50"), billing_cycle="monthly", currency="EUR", ) db.add(plan) await db.flush() db.add(SoftwareExternalHolder( id=uuid.uuid4(), integration_id=integ.id, software_id=cat.id, plan_id=plan.id, provider="euvendor", email="eu@x.com", category="off_paid_site", )) await db.flush() r = await gov.savings_opportunities(db) assert Decimal(r["waste_monthly_usd"]) == Decimal("0.00") # not booked at 1.0 assert r["unconverted_count"] == 1 assert r["fx_available"] is False seat = next(s for s in r["wasted_seats"] if s["email"] == "eu@x.com") assert seat["monthly_usd"] is None assert seat["currency"] == "EUR"