"""PR-B — promote non-IT expense into the ledger + external reconcile. Verifies the no-double-count promotion (only promote_to_ledger rows become cost_type='expense' in the unified ledger) and the internal-vs-external reconciliation view. """ from __future__ import annotations from decimal import Decimal import pytest import pytest_asyncio from app.core.coordinators import finance_spend_coordinator as coord from app.domains.finance import read_service from app.domains.finance.models import FinanceExternalStatement, FinanceSpendFact PERIOD = "2026-05" @pytest_asyncio.fixture async def seeded(db_session): db = db_session # collect_all_rows runs the AI collector, which requires the whitelist # platform setting (migration 149 seeds it in real envs). from app.integrations.models import PlatformSetting db.add(PlatformSetting(key="ai_spending.service_whitelist", value={"aws": ["Bedrock"]})) db.add(PlatformSetting(key="ai_spending.cny_to_usd_rate", value=0.14)) def ext(doc, line, cat, usd, *, promote, cc="CC-RD", vendor="携程", invoice_verified=None, payment_status=None): return FinanceExternalStatement( statement_key=f"k-{doc}-{line}", source="huilianyi", external_doc_no=doc, line_no=line, period=PERIOD, expense_category=cat, mapped_cost_type=("expense" if promote else None), promote_to_ledger=promote, cost_center=cc, department="研发", vendor=vendor, amount_native=Decimal(usd), currency="USD", amount_usd=Decimal(usd), fx_basis="native_usd", invoice_verified=invoice_verified, payment_status=payment_status, ) # Two promoted non-IT expense lines ($100 + $40) + one IT-overlap # line ($60, promote=False — reconciliation-only). db.add_all([ ext("BX-1", 0, "差旅费", "100", promote=True, payment_status="PAID"), ext("BX-1", 1, "餐饮费", "40", promote=True), ext("BX-2", 0, "云服务费", "60", promote=False, vendor="AWS", invoice_verified=True, payment_status="PAID"), ]) # Internal computed cloud spend in the same cost centre ($500). db.add(FinanceSpendFact( fact_key="f-cloud-ccrd", period=PERIOD, cost_type="cloud", basis="consumption", sub_category="EC2", vendor="aws", department="研发", cost_center="CC-RD", allocation_source="scope_owner", amount_native=Decimal("500"), currency_native="USD", amount_usd=Decimal("500"), fx_basis="native_usd", is_provisional=True, source_domain="cmdb", rollup_fingerprint="x", )) await db.flush() return db @pytest.mark.asyncio async def test_collect_expense_rows_only_promoted(seeded): rows = await coord.collect_expense_rows(seeded, PERIOD) # Only the two promote=True lines become ledger expense rows. assert len(rows) == 2 assert all(r.cost_type == "expense" for r in rows) total = sum(r.amount_usd for r in rows) assert total == Decimal("140.000000") # 100 + 40, NOT the $60 IT line cats = {r.sub_category for r in rows} assert cats == {"差旅费", "餐饮费"} @pytest.mark.asyncio async def test_collect_all_rows_includes_expense(seeded): # collect_all_rows recomputes the IT streams from SOURCE systems # (cmdb_cost_facts etc.), which this fixture doesn't seed — so we only # assert the expense stream is wired in (it reads the external ledger, # which IS seeded). The cloud finance_spend_facts row is the # materialised OUTPUT, not a collector input. rows = await coord.collect_all_rows(seeded, PERIOD) by_type = {} for r in rows: by_type[r.cost_type] = by_type.get(r.cost_type, Decimal("0")) + r.amount_usd assert by_type.get("expense") == Decimal("140.000000") @pytest.mark.asyncio async def test_external_reconcile_internal_vs_external(seeded): r = await read_service.external_reconcile(seeded, PERIOD, "cost_center") assert r["dim"] == "cost_center" row = next(x for x in r["rows"] if x["cost_center"] == "CC-RD") # Internal excludes the promoted 'expense' (would self-compare); only # the cloud $500 counts internal here. assert Decimal(row["internal_usd"]) == Decimal("500.00") # External = ALL statements (100 + 40 + 60). assert Decimal(row["external_usd"]) == Decimal("200.00") assert Decimal(row["diff_usd"]) == Decimal("300.00") assert row["ext_docs"] == 3 assert row["ext_invoices_verified"] == 1 assert row["ext_paid"] == 2 @pytest.mark.asyncio async def test_external_statements_browse_filters_promoted(seeded): only_recon = await read_service.external_statements(seeded, PERIOD, promoted=False) assert only_recon["total"] == 1 assert only_recon["rows"][0]["vendor"] == "AWS" promoted = await read_service.external_statements(seeded, PERIOD, promoted=True) assert promoted["total"] == 2