feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
557
server/tests/test_commercial_services.py
Normal file
557
server/tests/test_commercial_services.py
Normal file
@@ -0,0 +1,557 @@
|
||||
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}
|
||||
Reference in New Issue
Block a user