"""AliCloud International ``list_billing_daily`` 双口径回归(2026-07-28)。 镜像 ``test_aliyun_dual_measure.py`` 的 billing_daily pin:intl 覆写 此前构造 ``CloudDailyCost(... total_cost_usd=total_usd ...)`` 时**没有** 接 ``total_cost_usd_list``(父类 CN 路径已接 PretaxGrossAmount)—— 生产 intl 租户的 billing_daily 永远拿不到 list 口径,读侧 COALESCE(list, invoice) 长期回落实付。 Pins: * usage 行的 ``PretaxGrossAmount``(牌价)经与实付同一 FX 钩子换算 计入 list;public == list(阿里的牌价即 PretaxGrossAmount,与 CN 父类一致)。 * 调账/退款行不计入 list;缺牌价行计数 WARN,绝不用实付兜底。 * 全日无可计行 → ``total_cost_usd_list`` 为 None(口径未接入), 绝不伪造 0(driver_base 契约:None 透传)。 """ from __future__ import annotations from datetime import date from typing import Any import pytest from app.integrations.driver_base import CloudScopeRef from app.integrations.drivers.alicloud_intl_cloud import AlicloudIntlCloudDriver class _StubIntlDriver(AlicloudIntlCloudDriver): """Bypass ``__init__``;注入 BSS 行,pin FX 不触 DB。""" def __init__(self, items: list[dict[str, Any]]) -> None: self.integration_id = "test-alicloud-intl" self.config = {"cny_per_usd": 7.0} self._items = items async def _acs3_call(self, *, host: str, version: str, action: str, params: dict[str, Any]) -> dict[str, Any]: assert action == "DescribeInstanceBill" return {"Data": {"Items": self._items, "NextToken": None}} async def _master_account_id(self) -> str: return "5000000000000001" def _scope() -> CloudScopeRef: return CloudScopeRef( scope_type="aliyun_account", external_id="5000000000000001", ) @pytest.mark.asyncio async def test_intl_billing_daily_emits_list_measures(caplog) -> None: items = [ # USD usage 行:牌价 20 折后 14。 { "Item": "PayAsYouGoBill", "OwnerID": "5000000000000001", "PretaxAmount": "14.0", "PretaxGrossAmount": "20.0", "Currency": "USD", "ProductName": "ECS", }, # CNY usage 行:牌价 70 CNY → 10 USD(与实付同一 FX 钩子)。 { "Item": "PayAsYouGoBill", "OwnerID": "5000000000000001", "PretaxAmount": "35.0", "PretaxGrossAmount": "70.0", "Currency": "CNY", "ProductName": "OSS", }, # 调账负数行:list 恒不计。 { "Item": "Refund", "OwnerID": "5000000000000001", "PretaxAmount": "-6.0", "PretaxGrossAmount": "0", "Currency": "USD", "ProductName": "OSS", }, # usage 行缺牌价 → 计数 WARN,绝不用实付兜底。 { "Item": "PayAsYouGoBill", "OwnerID": "5000000000000001", "PretaxAmount": "3.0", "Currency": "USD", "ProductName": "RDS", }, ] drv = _StubIntlDriver(items) with caplog.at_level("WARNING"): rows = await drv.list_billing_daily( date(2026, 7, 21), date(2026, 7, 21), scope=_scope(), ) assert len(rows) == 1 r = rows[0] # 发票口径(USD 已换算):14 + 35/7 − 6 + 3 = 16。 assert r.total_cost_usd == pytest.approx(16.0) assert r.currency == "USD" # 研发口径:仅 usage 行的牌价,USD 换算:20 + 70/7 = 30;调账与 # 缺牌价行不入。public == list。 assert r.total_cost_usd_list == pytest.approx(30.0) assert r.total_cost_usd_public == pytest.approx(30.0) assert any( "PretaxGrossAmount" in rec.message for rec in caplog.records ) @pytest.mark.asyncio async def test_intl_billing_daily_list_none_when_no_countable_row() -> None: """全日无可计 list 行(usage 全缺牌价)→ None,绝不伪造 0。""" items = [ { "Item": "PayAsYouGoBill", "OwnerID": "5000000000000001", "PretaxAmount": "3.0", "Currency": "USD", "ProductName": "RDS", }, ] drv = _StubIntlDriver(items) rows = await drv.list_billing_daily( date(2026, 7, 21), date(2026, 7, 21), scope=_scope(), ) assert len(rows) == 1 assert rows[0].total_cost_usd == pytest.approx(3.0) assert rows[0].total_cost_usd_list is None assert rows[0].total_cost_usd_public is None @pytest.mark.asyncio async def test_intl_billing_daily_list_none_during_fx_outage( monkeypatch, caplog, ) -> None: """FX 停摆(cny_per_usd 拿不到)时,CNY 行的牌价「换不出来」≠「换出 来是 0」。 ``_convert_pretax_amount`` 对 CNY + 无汇率刻意返回 ``(0.0, "CNY")`` —— 实付列必须留行(amount_usd=0 + 原币金额另存)。牌价列没有原币伴生 列,吞下这个 0.0 就等于宣布「当日牌价 $0」:读侧 ``daily_engineering_amount()`` = COALESCE(list, invoice) 会报 $0, 而当天还有 USD 行的真实支出($100)。本子类 ``requires_cny_fx = False`` 正是为了让 FX 停摆不杀掉整次摄取,所以该状态按设计可达。 """ async def _boom(*_a, **_kw): raise RuntimeError("FX setting unavailable") import app.core.fx_rate as fx_module monkeypatch.setattr(fx_module, "resolve_cny_per_usd", _boom) drv = _StubIntlDriver([ # USD usage 行:真实支出 $100,无牌价。 { "Item": "PayAsYouGoBill", "OwnerID": "5000000000000001", "PretaxAmount": "100.0", "Currency": "USD", "ProductName": "ECS", }, # CNY usage 行:有牌价,但无汇率可换。 { "Item": "PayAsYouGoBill", "OwnerID": "5000000000000001", "PretaxAmount": "700.0", "PretaxGrossAmount": "1400.0", "Currency": "CNY", "ProductName": "OSS", }, ]) drv.config = {} # 逼走平台 FX 设置这条路 → 被 monkeypatch 打断 rows = await drv.list_billing_daily( date(2026, 7, 21), date(2026, 7, 21), scope=_scope(), ) assert len(rows) == 1 assert rows[0].total_cost_usd == pytest.approx(100.0) # 修复前:0.0(两个 CNY 行的 0.0 置了 list_seen)。 assert rows[0].total_cost_usd_list is None assert rows[0].total_cost_usd_public is None