"""Finance self-service config tests: config singleton, line-def + KPI-def + account-map editing.""" from __future__ import annotations import uuid from decimal import Decimal import pytest from pydantic import ValidationError from app.domains.finance_flux import config_service as cfg from app.domains.finance_flux import constants as c from app.domains.finance_flux.models import ( FinanceFluxAccountMap, FinanceFluxKpiDef, FinanceFluxLineDef, ) from app.domains.finance_flux.schemas import ConfigUpdateBody pytestmark = pytest.mark.asyncio def test_config_currency_validation_rejects_non_iso() -> None: """Regression: ``currency`` lands in a CHAR(3) column — reject anything that isn't a 3-letter code at the edge (422) instead of a DB DataError 500, and normalise case so downstream formatting is stable.""" assert ConfigUpdateBody(currency="usd").currency == "USD" assert ConfigUpdateBody().currency is None for bad in ("DOLLARS", "US", ""): with pytest.raises(ValidationError): ConfigUpdateBody(currency=bad) async def test_config_get_creates_and_update(db_session): # get on an empty catalog returns sane defaults (and seeds a row) got = await cfg.get_config(db_session) await db_session.commit() assert got["amount_threshold"] == "10000.00" assert got["pct_threshold"] == "20.00" out = await cfg.update_config( db_session, amount_threshold=Decimal("25000"), ai_enabled=False, comparison_modes=[c.COMPARE_MOM], editor_id=None) await db_session.commit() assert out["amount_threshold"] == "25000.00" assert out["ai_enabled"] is False assert out["comparison_modes"] == ["mom"] async def test_update_line_def_owner_and_threshold(db_session): d = FinanceFluxLineDef( id=uuid.uuid4(), statement=c.STATEMENT_BS, line_key="cash_and_equivalents", label="Cash and cash equivalents", row_type=c.ROW_LINE, sort_order=0) db_session.add(d) await db_session.commit() out = await cfg.update_line_def( db_session, line_def_id=str(d.id), owner_label="Eelin", threshold_amount=Decimal("50000"), is_active=True) await db_session.commit() assert out["owner_label"] == "Eelin" assert out["threshold_amount"] == "50000.00" async def test_update_kpi_and_account_map(db_session): k = FinanceFluxKpiDef( id=uuid.uuid4(), kpi_key="total_assets", label="Total Assets", statement=c.STATEMENT_BS, source_line_key="total_assets", sort_order=0) m = FinanceFluxAccountMap( id=uuid.uuid4(), ns_account_id="3485", account_number="1002050101_US", statement=c.STATEMENT_BS, sign=1) db_session.add_all([k, m]) await db_session.commit() ko = await cfg.update_kpi_def( db_session, kpi_def_id=str(k.id), label="总资产", higher_is_better=True) mo = await cfg.update_account_map_entry( db_session, entry_id=str(m.id), line_key="cash_and_equivalents", sign=-1) await db_session.commit() assert ko["label"] == "总资产" assert mo["line_key"] == "cash_and_equivalents" assert mo["sign"] == -1 unmapped = await cfg.list_account_map(db_session, unmapped_only=True) assert all(e["line_key"] is None for e in unmapped) # the one we mapped is excluded def test_threshold_bounds_reject_overflow(): # Numeric(7,2) max 99999.99 — values that would round to overflow must be # rejected at the edge (422) not 500 on a DB DataError. (Closeout review.) import pytest as _pytest from pydantic import ValidationError from app.domains.finance_flux.schemas import ConfigUpdateBody, LineDefUpdateBody # (body, pct-field, amount-field) — the two bodies name these differently. for body, pct, amt in ( (ConfigUpdateBody, "pct_threshold", "amount_threshold"), (LineDefUpdateBody, "threshold_pct", "threshold_amount"), ): with _pytest.raises(ValidationError): body(**{pct: Decimal("99999.999")}) # rounds to 100000.00 → overflow with _pytest.raises(ValidationError): body(**{pct: Decimal("-1")}) with _pytest.raises(ValidationError): body(**{amt: Decimal("1e17")}) # A sane value passes. assert ConfigUpdateBody(pct_threshold=Decimal("20"), amount_threshold=Decimal("10000")).pct_threshold == Decimal("20")