"""Tests for the AI plan-currency guard (PR-2). Pins the detection contract: * a `category='ai'` plan whose currency is mis-entered (CNY) while it has active seats → ``non_usd_currency_with_active_seats``; * a normal USD plan → no finding; * an implausibly-priced USD plan → ``implausible_per_seat_usd``; * ``has_suspicious_ai_seat_currency`` is the cheap dashboard predicate; * ``cost_rollup_drift`` carries the ``plan_currency`` sub-block. """ from __future__ import annotations from datetime import UTC, datetime from decimal import Decimal from unittest.mock import AsyncMock, patch from uuid import uuid4 import pytest from app.domains.ai_spending.cost_drift import ( cost_rollup_drift, has_suspicious_ai_seat_currency, plan_currency_findings, ) from app.domains.ai_spending.service import AiSpendingService from app.domains.asset.models import ( AssetSoftwareCatalog, SoftwareLicensePlan, SoftwareSeatAllocation, ) from app.identity.person_models import IdentityPerson async def _ai_software(db) -> AssetSoftwareCatalog: sw = AssetSoftwareCatalog( id=uuid4(), name=f"AI {uuid4().hex[:6]}", status="active", category="ai", ) db.add(sw) await db.flush() return sw async def _plan(db, sw, *, currency: str, price: str, cycle: str = "monthly"): plan = SoftwareLicensePlan( id=uuid4(), software_id=sw.id, name=f"plan-{currency}-{price}", price=Decimal(price), billing_cycle=cycle, currency=currency, max_seats=None, status="active", ) db.add(plan) await db.flush() return plan async def _active_seat(db, plan) -> None: person = IdentityPerson( source_system="test", hris_person_id=f"P{uuid4().hex[:6]}", employee_no=f"E{uuid4().hex[:6]}", work_email=f"{uuid4().hex[:8]}@test.local", display_name="seat holder", employment_status="active", ) db.add(person) await db.flush() db.add(SoftwareSeatAllocation( id=uuid4(), plan_id=plan.id, user_id=person.id, status="active", allocated_at=datetime(2026, 1, 1, tzinfo=UTC), )) await db.flush() @pytest.fixture def _stub_cny_rate(): # Pin FX so per-seat USD is deterministic (USD per CNY ≈ 0.1479). # plan_currency_findings reads the None-returning fx_rate source directly. with patch( "app.core.fx_rate.get_usd_per_cny", AsyncMock(return_value=0.1479), ): yield @pytest.mark.asyncio async def test_cny_misentry_with_active_seats_flagged(db_session, _stub_cny_rate): sw = await _ai_software(db_session) plan = await _plan(db_session, sw, currency="CNY", price="20") await _active_seat(db_session, plan) out = await plan_currency_findings(db_session) rows = [f for f in out["findings"] if f["plan_id"] == str(plan.id)] assert len(rows) == 1 assert "non_usd_currency_with_active_seats" in rows[0]["reasons"] assert rows[0]["currency"] == "CNY" assert rows[0]["active_seats"] == 1 @pytest.mark.asyncio async def test_normal_usd_plan_not_flagged(db_session, _stub_cny_rate): sw = await _ai_software(db_session) plan = await _plan(db_session, sw, currency="USD", price="30") await _active_seat(db_session, plan) out = await plan_currency_findings(db_session) assert [f for f in out["findings"] if f["plan_id"] == str(plan.id)] == [] @pytest.mark.asyncio async def test_implausible_usd_price_flagged(db_session, _stub_cny_rate): sw = await _ai_software(db_session) # $5000/seat/month USD — above the $1000 plausibility ceiling. plan = await _plan(db_session, sw, currency="USD", price="5000") out = await plan_currency_findings(db_session) rows = [f for f in out["findings"] if f["plan_id"] == str(plan.id)] assert len(rows) == 1 assert "implausible_per_seat_usd" in rows[0]["reasons"] @pytest.mark.asyncio async def test_non_usd_without_active_seats_not_flagged(db_session, _stub_cny_rate): # CNY plan but NO active seats + plausible per-seat → no finding. sw = await _ai_software(db_session) await _plan(db_session, sw, currency="CNY", price="200") # ~$29.6/seat out = await plan_currency_findings(db_session) assert out["findings"] == [] @pytest.mark.asyncio async def test_has_suspicious_predicate(db_session, _stub_cny_rate): sw = await _ai_software(db_session) assert await has_suspicious_ai_seat_currency(db_session) is False usd_plan = await _plan(db_session, sw, currency="USD", price="30") await _active_seat(db_session, usd_plan) assert await has_suspicious_ai_seat_currency(db_session) is False cny_plan = await _plan(db_session, sw, currency="CNY", price="20") await _active_seat(db_session, cny_plan) assert await has_suspicious_ai_seat_currency(db_session) is True @pytest.mark.asyncio async def test_cost_drift_includes_plan_currency_block(db_session, _stub_cny_rate): sw = await _ai_software(db_session) plan = await _plan(db_session, sw, currency="CNY", price="20") await _active_seat(db_session, plan) with patch.object( AiSpendingService, "_compute_period_rows", AsyncMock(return_value=[]), ), patch.object( AiSpendingService, "_rollup_fingerprint", AsyncMock(return_value="FP"), ): out = await cost_rollup_drift(db_session, months_back=1) assert "plan_currency" in out assert any( f["plan_id"] == str(plan.id) for f in out["plan_currency"]["findings"] )