from __future__ import annotations import uuid from datetime import UTC, date, datetime, timedelta from decimal import Decimal import pytest from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.pool import StaticPool import app.models # noqa: F401 - 注册完整 metadata from app.api.deps import CurrentUserContext from app.db.base_class import Base from app.models.financial_record import ExpenseClaim, ExpenseClaimItem from app.schemas.commercial import ( CommercialCostEventCreate, CommercialEntitlementUpsert, CommercialPlanCreate, CommercialPricingScenarioWrite, CommercialSubscriptionCreate, CommercialSubscriptionTransition, UsageMeterEventCreate, ) from app.schemas.savings import ( SavingsEvidenceCreate, SavingsRealizationActionCreate, SavingsRealizationCreate, ) from app.services.commercial_access_policy import CommercialConflictError from app.services.commercial_admin import CommercialAdminService from app.services.commercial_analytics import CommercialAnalyticsService from app.services.commercial_entitlements import CommercialEntitlementService from app.services.commercial_metering import CommercialMeteringService from app.services.commercial_pricing import CommercialPricingService from app.services.commercial_queries import CommercialQueryService from app.services.savings_discovery import SavingsDiscoveryService from app.services.savings_realization import SavingsRealizationService @pytest.fixture() def db() -> Session: engine = create_engine( "sqlite+pysqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) Base.metadata.create_all(engine) factory = sessionmaker(bind=engine, expire_on_commit=False) with factory() as session: yield session Base.metadata.drop_all(engine) engine.dispose() def test_commercial_account_quota_idempotency_and_security_gate(db: Session) -> None: now = datetime.now(UTC) _, subscription, entitlement = _seed_commercial_account(db, "tenant-a", now) service = CommercialMeteringService(db) payload = UsageMeterEventCreate( subscription_id=subscription.id, entitlement_id=entitlement.id, quantity=Decimal("2"), occurred_at=now, source_system="agent-runtime", idempotency_key="usage-001", subject_type="agent_run", subject_id="run-001", ) created, was_created = service.record_usage( "tenant-a", payload, actor_type="system", actor_id="agent-runtime", ) replay, replay_created = service.record_usage( "tenant-a", payload, actor_type="system", actor_id="agent-runtime", ) assert was_created is True assert replay_created is False assert replay.id == created.id with pytest.raises(CommercialConflictError, match="幂等键"): service.record_usage( "tenant-a", payload.model_copy(update={"quantity": Decimal("3")}), actor_type="system", actor_id="agent-runtime", ) account = CommercialEntitlementService(db).get_account( _user("finance-a", tenant_id="tenant-a", roles=["finance"]), as_of=now + timedelta(seconds=1), ) assert account.tenant_id == "tenant-a" assert account.quotas[0].used_quantity == Decimal("2") assert account.quotas[0].hard_limit_remaining == Decimal("4") commercial_gate = CommercialEntitlementService(db).check( "tenant-a", entitlement_key="ai_precheck", requested_quantity=Decimal("1"), security_decision="human_review", as_of=now + timedelta(seconds=1), ) assert commercial_gate.commercial_allowed is True assert commercial_gate.final_allowed is False assert "人工审核" in commercial_gate.reason with pytest.raises(CommercialConflictError, match="硬配额"): service.record_usage( "tenant-a", payload.model_copy( update={ "quantity": Decimal("5"), "idempotency_key": "usage-over-limit", } ), actor_type="system", actor_id="agent-runtime", ) with pytest.raises(CommercialConflictError, match="不能回改配额"): CommercialAdminService(db).upsert_entitlement( "tenant-a", CommercialEntitlementUpsert( subscription_id=subscription.id, entitlement_key="ai_precheck", metric_key="ai_precheck_runs", entitlement_type="metered", unit="run", included_quantity=Decimal("5"), hard_limit_quantity=Decimal("8"), reset_interval="monthly", overage_policy="block", effective_from=now - timedelta(days=10), ), ) with pytest.raises(LookupError): service.record_usage( "tenant-b", payload.model_copy(update={"idempotency_key": "cross-tenant"}), actor_type="system", actor_id="agent-runtime", ) def test_cost_events_are_server_derived_idempotent_and_reversible(db: Session) -> None: now = datetime.now(UTC) _, subscription, _ = _seed_commercial_account(db, "tenant-a", now) payload = CommercialCostEventCreate( subscription_id=subscription.id, event_type="incurred", cost_category="ai_inference", quantity=Decimal("200"), unit="1k_tokens", unit_cost=Decimal("0.1"), original_currency="CNY", reporting_currency="CNY", fx_rate=Decimal("1"), provider="provider-a", model_name="model-a", allocation_key="tenant-a:ai", occurred_at=now, source_system="provider-billing", idempotency_key="cost-001", ) service = CommercialMeteringService(db) event, created = service.record_cost("tenant-a", payload) replay, replay_created = service.record_cost("tenant-a", payload) assert created is True assert replay_created is False assert replay.id == event.id assert event.cost_amount == Decimal("20.0000") assert event.reporting_amount == Decimal("20.0000") with pytest.raises(CommercialConflictError, match="幂等键"): service.record_cost( "tenant-a", payload.model_copy(update={"quantity": Decimal("201")}), ) reversal_payload = payload.model_copy( update={ "event_type": "reversal", "idempotency_key": "cost-reversal-001", "reversal_of_cost_event_id": event.id, "occurred_at": now + timedelta(minutes=1), } ) reversal, _ = service.record_cost("tenant-a", reversal_payload) assert reversal.cost_amount == Decimal("-20.0000") assert reversal.reporting_amount == Decimal("-20.0000") with pytest.raises(CommercialConflictError, match="已经冲回"): service.record_cost( "tenant-a", reversal_payload.model_copy(update={"idempotency_key": "cost-reversal-002"}), ) def test_commercial_analytics_separates_charge_cost_savings_and_currency(db: Session) -> None: now = datetime.now(UTC) _, subscription, _ = _seed_commercial_account(db, "tenant-a", now) CommercialMeteringService(db).record_cost( "tenant-a", _cost_payload(subscription.id, now, "CNY", Decimal("20"), "cost-cny"), ) CommercialMeteringService(db).record_cost( "tenant-a", _cost_payload(subscription.id, now, "USD", Decimal("5"), "cost-usd"), ) _seed_verified_savings(db, "tenant-a", now, Decimal("200"), "CNY") db.commit() result = CommercialAnalyticsService(db).build( "tenant-a", start=now - timedelta(days=2), end=now + timedelta(days=2), as_of=now + timedelta(days=1), ) assert result.customer_charges.status == "partial" assert _money(result.customer_charges) == {"CNY": Decimal("100.0000")} assert _money(result.internal_costs) == { "CNY": Decimal("20.0000"), "USD": Decimal("5.0000"), } assert _money(result.contribution_margin) == {"CNY": Decimal("80.0000")} assert result.contribution_margin.status == "partial" assert result.verified_cash_savings.status == "available" assert _money(result.verified_cash_savings) == {"CNY": Decimal("200.0000")} assert result.customer_roi.ratios[0].currency == "CNY" assert result.customer_roi.ratios[0].ratio == Decimal("1.000000") assert result.customer_roi.ratios[0].numerator == Decimal("100.0000") assert result.customer_labor_value.status == "unavailable" assert any("币种" in note for note in result.contribution_margin.notes) unavailable = CommercialAnalyticsService(db).build( "tenant-b", start=now - timedelta(days=2), end=now + timedelta(days=2), as_of=now + timedelta(days=1), ) assert unavailable.customer_charges.status == "unavailable" assert unavailable.customer_charges.values == [] assert unavailable.internal_costs.status == "unavailable" assert unavailable.customer_roi.status == "unavailable" def test_commercial_write_contract_rejects_ambiguous_naive_timestamps() -> None: with pytest.raises(ValueError, match="时区"): UsageMeterEventCreate( subscription_id=str(uuid.uuid4()), entitlement_id=str(uuid.uuid4()), quantity=Decimal("1"), occurred_at=datetime(2026, 7, 16, 12, 0), source_system="ambiguous-clock", idempotency_key="naive-time", ) def test_subscription_lifecycle_and_history_queries_are_operable(db: Session) -> None: now = datetime.now(UTC) plan, subscription, entitlement = _seed_commercial_account(db, "tenant-a", now) admin = CommercialAdminService(db) suspended = admin.transition_subscription( "tenant-a", subscription.id, CommercialSubscriptionTransition( expected_version=subscription.version, target_status="suspended", reason="客户要求暂时停用自动续费与用量消费", ), actor_id="platform-admin", ) assert suspended.status == "suspended" assert suspended.metadata_json["status_history"][-1]["to"] == "suspended" resumed = admin.activate_subscription( "tenant-a", subscription.id, expected_version=suspended.version, ) assert resumed.status == "active" canceled = admin.transition_subscription( "tenant-a", subscription.id, CommercialSubscriptionTransition( expected_version=resumed.version, target_status="canceled", reason="试点合同到期且客户确认不再续约", ), actor_id="platform-admin", ) assert canceled.status == "canceled" assert canceled.canceled_at is not None assert canceled.ends_at is not None with pytest.raises(CommercialConflictError, match="不能从"): admin.transition_subscription( "tenant-a", subscription.id, CommercialSubscriptionTransition( expected_version=canceled.version, target_status="suspended", reason="终态不可恢复", ), actor_id="platform-admin", ) query = CommercialQueryService(db) assert [row.id for row in query.list_plans("tenant-a")] == [plan.id] assert [row.id for row in query.list_subscriptions("tenant-a")] == [subscription.id] assert [row.id for row in query.list_entitlements("tenant-a")] == [entitlement.id] assert query.list_plans("tenant-b") == [] with pytest.raises(ValueError, match="时区"): query.list_usage_events( "tenant-a", start=datetime(2026, 7, 16, 0, 0), ) def test_pricing_scenario_uses_cost_floor_and_verified_value_ceiling(db: Session) -> None: now = datetime.now(UTC) _, subscription, _ = _seed_commercial_account(db, "tenant-a", now) CommercialMeteringService(db).record_cost( "tenant-a", _cost_payload(subscription.id, now, "CNY", Decimal("20"), "pricing-cost"), ) _seed_verified_savings(db, "tenant-a", now, Decimal("200"), "CNY") db.commit() result = CommercialPricingService(db).build( "tenant-a", CommercialPricingScenarioWrite( start=now - timedelta(days=2), end=now + timedelta(days=2), as_of=now + timedelta(days=1), target_contribution_margin_rate=Decimal("0.5"), max_verified_savings_share=Decimal("0.25"), ), ) assert result.recommended_model == "hybrid" assert result.evidence_status == "complete" scenario = result.scenarios[0] assert scenario.status == "feasible" assert scenario.minimum_sustainable_charge == Decimal("40.0000") assert scenario.maximum_value_aligned_charge == Decimal("50.0000") assert scenario.maximum_success_fee == Decimal("10.0000") assert scenario.customer_roi_at_minimum_charge == Decimal("4.000000") assert scenario.contribution_margin_at_value_ceiling == Decimal("0.600000") conflict = CommercialPricingService(db).build( "tenant-a", CommercialPricingScenarioWrite( start=now - timedelta(days=2), end=now + timedelta(days=2), as_of=now + timedelta(days=1), target_contribution_margin_rate=Decimal("0.5"), max_verified_savings_share=Decimal("0.05"), ), ) assert conflict.recommended_model == "optimize_unit_economics" assert conflict.scenarios[0].status == "insufficient_value" def _seed_commercial_account(db: Session, tenant_id: str, now: datetime): admin = CommercialAdminService(db) plan = admin.create_plan( tenant_id, CommercialPlanCreate( plan_code="growth", name="成长版", pricing_model="subscription", billing_interval="monthly", currency="CNY", base_fee=Decimal("100"), included_seats=10, effective_from=now - timedelta(days=30), ), actor_id="platform-admin", ) admin.activate_plan(tenant_id, plan.id, expected_version=plan.version) subscription = admin.create_subscription( tenant_id, CommercialSubscriptionCreate( subscription_key=f"{tenant_id}-2026", plan_id=plan.id, starts_at=now - timedelta(days=10), current_period_start=now - timedelta(days=1), current_period_end=now + timedelta(days=29), seats=5, ), actor_id="platform-admin", ) entitlement = admin.upsert_entitlement( tenant_id, CommercialEntitlementUpsert( subscription_id=subscription.id, entitlement_key="ai_precheck", metric_key="ai_precheck_runs", entitlement_type="metered", unit="run", included_quantity=Decimal("5"), hard_limit_quantity=Decimal("6"), reset_interval="monthly", overage_policy="block", effective_from=now - timedelta(days=10), ), ) db.flush() return plan, subscription, entitlement def _cost_payload( subscription_id: str, now: datetime, currency: str, amount: Decimal, key: str, ) -> CommercialCostEventCreate: return CommercialCostEventCreate( subscription_id=subscription_id, cost_category="infrastructure", quantity=Decimal("1"), unit="allocation", unit_cost=amount, original_currency=currency, reporting_currency=currency, fx_rate=Decimal("1"), allocation_key=key, occurred_at=now, source_system="cost-ledger", idempotency_key=key, ) def _seed_verified_savings( db: Session, tenant_id: str, now: datetime, amount: Decimal, currency: str, ) -> None: operator = _user("finance-operator", tenant_id=tenant_id, roles=["finance"]) confirmer = _user("finance-confirmer", tenant_id=tenant_id, roles=["finance"]) original = Decimal("1000") claim = ExpenseClaim( id=str(uuid.uuid4()), claim_no=f"BX-{uuid.uuid4().hex[:12]}", employee_name="测试员工", department_name="销售部", project_code="COMMERCIAL-ROI", expense_type="hotel", reason="客户现场差旅", location="上海", amount=original, currency=currency, invoice_count=1, occurred_at=now, status="draft", risk_flags_json=[], ) item = ExpenseClaimItem( id=str(uuid.uuid4()), claim=claim, item_date=date.today(), item_type="hotel", item_reason="住宿", item_location="上海", item_note="", item_amount=original, ) db.add(claim) db.flush() opportunity = SavingsDiscoveryService(db).discover_standard_adjustments( claim=claim, items_by_id={item.id: item}, adjustment_flags=[ { "item_id": item.id, "message": "商业 ROI 测试政策差额", "original_amount": str(original), "reimbursable_amount": str(original - amount), "employee_absorbed_amount": str(amount), "policy_rule_version": "commercial-roi-v1", "policy_grade": "P6", "policy_matched_city": "上海", "calculation_fingerprint": "sha256:" + uuid.uuid4().hex * 2, } ], current_user=operator, request_id=f"discover-{uuid.uuid4().hex[:8]}", )[0] realization = ( SavingsRealizationService(db) .record( opportunity.id, SavingsRealizationCreate( request_id=f"record-{uuid.uuid4().hex[:8]}", expected_version=1, comment="登记可追溯实际节省", actual_gross=amount, incremental_cost=Decimal("0"), currency=currency, realized_at=now, attribution_method="server_policy_counterfactual", attribution_ratio=Decimal("1"), evidence_level="business_state", evidence=[ SavingsEvidenceCreate( evidence_key=f"payment-{uuid.uuid4().hex}", evidence_role="payment_business_state", resource_type="business_event", resource_id=f"payment-{uuid.uuid4().hex}", source_system="x-financial", content_hash="c" * 64, occurred_at=now, verification_status="unverified", ) ], ), operator, ) .response.realization ) SavingsRealizationService(db).execute_action( realization.id, SavingsRealizationActionCreate( action="confirm", request_id=f"confirm-{uuid.uuid4().hex[:8]}", expected_version=1, comment="独立财务确认商业 ROI 事实", ), confirmer, ) def _user( username: str, *, tenant_id: str, roles: list[str] | None = None, is_admin: bool = False, ) -> CurrentUserContext: return CurrentUserContext( username=username, name=username, role_codes=list(roles or []), is_admin=is_admin, tenant_id=tenant_id, employee_id=username, ) def _money(metric) -> dict[str, Decimal]: return {item.currency: item.amount for item in metric.values}