"""Dual cost measure schema contract (migration 429, PR1 of the 成本口径改造 series). Pins the invariants every later PR builds on: * ``CostFact`` defaults: the new list/public measures default to ``None`` and ``cost_bucket`` defaults to ``'usage'`` — a driver that predates the whitelist produces rows indistinguishable from "measure not wired", never a fabricated $0 list cost. * ``COST_BUCKETS`` vocabulary is closed and contains every constant. * ``cmdb_cost_facts`` / ``cmdb_cloud_billing_daily`` accept and round-trip the new columns (Decimal precision preserved), and pre-existing insert paths that don't mention the columns still work (server defaults / NULL). * The engineering measure aggregation contract: SUM(amount_usd_list) over a mixed bag of rows counts ONLY whitelisted usage rows — discount rows carry NULL and drop out without any row filtering. """ from __future__ import annotations import uuid from datetime import date from decimal import Decimal import pytest import pytest_asyncio from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.domains.cmdb.models import CmdbCloudBillingDaily, CmdbCostFact from app.integrations.driver_base import ( COST_BUCKET_COMMITMENT_DISCOUNT, COST_BUCKET_USAGE, COST_BUCKETS, CloudDailyCost, CostFact, ) from app.integrations.models import Integration def test_cost_fact_dataclass_defaults() -> None: fact = CostFact( date=date(2026, 7, 1), integration_id="i-1", service="EC2 - Other", amount_usd=1.5, amount_native=1.5, currency="USD", ) assert fact.amount_usd_list is None assert fact.amount_native_list is None assert fact.amount_usd_public is None assert fact.cost_bucket == COST_BUCKET_USAGE def test_cloud_daily_cost_dataclass_defaults() -> None: row = CloudDailyCost(date=date(2026, 7, 1), total_cost_usd=10.0) assert row.total_cost_usd_list is None assert row.total_cost_usd_public is None def test_cost_bucket_vocabulary_closed() -> None: # Every canonical constant is in the closed set; the set has no # strays (a typo'd bucket in a driver should fail membership). assert COST_BUCKET_USAGE in COST_BUCKETS assert COST_BUCKET_COMMITMENT_DISCOUNT in COST_BUCKETS assert COST_BUCKETS == { "usage", "commitment_fee", "commitment_discount", "contract_discount", "credit", "adjustment", "tax", "support", "other", } @pytest_asyncio.fixture async def dual_measure_integration(db_session: AsyncSession) -> Integration: integ = Integration( id=str(uuid.uuid4()), kind="cloud", provider="aws", name="AWS dual-measure test", slug=f"aws-dual-{uuid.uuid4().hex[:8]}", enabled=True, ) db_session.add(integ) await db_session.commit() # NOTE: no refresh() — ``Integration.id`` is a ``Uuid`` column, so a # refresh replaces the assigned str with a ``uuid.UUID`` object whose # bind serialization (dashless hex) does NOT match the dashed string # the String(36) FK columns store. Production code passes integration # ids around as str; the fixture mirrors that. return integ @pytest.mark.asyncio async def test_cost_fact_columns_roundtrip_and_sum_contract( db_session: AsyncSession, dual_measure_integration: Integration, ) -> None: """Mixed usage + discount rows: SUM(list) counts only usage rows.""" day = date(2026, 7, 1) usage = CmdbCostFact( fact_key=uuid.uuid4().hex, date=day, integration_id=dual_measure_integration.id, service="EC2 - Other", amount_usd=Decimal("7.000001"), amount_native=Decimal("7.000001"), currency="USD", amount_usd_list=Decimal("7.000001"), amount_native_list=Decimal("7.000001"), amount_usd_public=Decimal("8.500000"), cost_bucket="usage", ) # Discount row: list measures stay NULL, bucket classifies it. negation = CmdbCostFact( fact_key=uuid.uuid4().hex, date=day, integration_id=dual_measure_integration.id, service="Savings Plans", amount_usd=Decimal("-7.000001"), amount_native=Decimal("-7.000001"), currency="USD", cost_bucket="commitment_discount", ) # Legacy-style insert: no dual-measure kwargs at all — must land # with NULL list measures + 'usage' bucket via defaults. legacy = CmdbCostFact( fact_key=uuid.uuid4().hex, date=day, integration_id=dual_measure_integration.id, service="S3", amount_usd=Decimal("1.25"), amount_native=Decimal("1.25"), currency="USD", ) db_session.add_all([usage, negation, legacy]) await db_session.commit() got = (await db_session.execute( select(CmdbCostFact).where(CmdbCostFact.date == day) .where(CmdbCostFact.integration_id == dual_measure_integration.id), )).scalars().all() assert len(got) == 3 by_service = {f.service: f for f in got} assert by_service["EC2 - Other"].amount_usd_list == Decimal("7.000001") assert by_service["EC2 - Other"].amount_usd_public == Decimal("8.500000") assert by_service["Savings Plans"].amount_usd_list is None assert by_service["Savings Plans"].cost_bucket == "commitment_discount" assert by_service["S3"].amount_usd_list is None assert by_service["S3"].cost_bucket == "usage" # The engineering-measure contract: bare SUM, no row filter. list_sum = (await db_session.execute( select(func.sum(CmdbCostFact.amount_usd_list)) .where(CmdbCostFact.date == day) .where(CmdbCostFact.integration_id == dual_measure_integration.id), )).scalar() assert list_sum == Decimal("7.000001") # Invoice measure unchanged: nets to ~$1.25 (usage − negation + s3). invoice_sum = (await db_session.execute( select(func.sum(CmdbCostFact.amount_usd)) .where(CmdbCostFact.date == day) .where(CmdbCostFact.integration_id == dual_measure_integration.id), )).scalar() assert invoice_sum == Decimal("1.250000") @pytest.mark.asyncio async def test_billing_daily_columns_roundtrip( db_session: AsyncSession, dual_measure_integration: Integration, ) -> None: row = CmdbCloudBillingDaily( integration_id=dual_measure_integration.id, scope_id=None, linked_account_id="123456789012", date=date(2026, 7, 1), total_cost_usd=Decimal("100.0000"), total_cost_usd_list=Decimal("130.5000"), total_cost_usd_public=Decimal("140.0000"), currency="USD", ) legacy = CmdbCloudBillingDaily( integration_id=dual_measure_integration.id, scope_id=None, linked_account_id="999999999999", date=date(2026, 7, 1), total_cost_usd=Decimal("5.0000"), currency="USD", ) db_session.add_all([row, legacy]) await db_session.commit() got = (await db_session.execute( select(CmdbCloudBillingDaily) .where(CmdbCloudBillingDaily.integration_id == dual_measure_integration.id), )).scalars().all() by_acct = {r.linked_account_id: r for r in got} assert by_acct["123456789012"].total_cost_usd_list == Decimal("130.5000") assert by_acct["123456789012"].total_cost_usd_public == Decimal("140.0000") assert by_acct["999999999999"].total_cost_usd_list is None assert by_acct["999999999999"].total_cost_usd_public is None