370 lines
12 KiB
Python
370 lines
12 KiB
Python
"""智能定价服务单元测试
|
||
|
||
测试 PricingService 的核心业务逻辑
|
||
"""
|
||
|
||
import pytest
|
||
from decimal import Decimal
|
||
from datetime import datetime
|
||
from unittest.mock import AsyncMock, patch, MagicMock
|
||
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.services.pricing_service import PricingService
|
||
from app.schemas.pricing import (
|
||
StrategyType, MarketReference, StrategySuggestion, PricingSuggestions
|
||
)
|
||
from app.models import Project, ProjectCostSummary, PricingPlan
|
||
|
||
|
||
class TestPricingService:
|
||
"""智能定价服务测试类"""
|
||
|
||
@pytest.mark.unit
|
||
def test_calculate_strategy_price_traffic(self):
|
||
"""测试引流款定价策略"""
|
||
service = PricingService(None)
|
||
|
||
suggestion = service.calculate_strategy_price(
|
||
base_cost=100.0,
|
||
strategy=StrategyType.TRAFFIC,
|
||
)
|
||
|
||
# 引流款利润率 10%-20%,使用中间值 15%
|
||
# 价格 = 100 / (1 - 0.15) ≈ 117.65
|
||
assert suggestion.strategy == "引流款"
|
||
assert suggestion.suggested_price > 100 # 大于成本
|
||
assert suggestion.suggested_price < 130 # 利润率适中
|
||
assert suggestion.margin > 0
|
||
assert "引流" in suggestion.description
|
||
|
||
@pytest.mark.unit
|
||
def test_calculate_strategy_price_profit(self):
|
||
"""测试利润款定价策略"""
|
||
service = PricingService(None)
|
||
|
||
suggestion = service.calculate_strategy_price(
|
||
base_cost=100.0,
|
||
strategy=StrategyType.PROFIT,
|
||
target_margin=50, # 50% 目标毛利率
|
||
)
|
||
|
||
# 价格 = 100 / (1 - 0.5) = 200
|
||
assert suggestion.strategy == "利润款"
|
||
assert suggestion.suggested_price >= 200
|
||
assert suggestion.margin >= 45 # 接近目标
|
||
assert "日常" in suggestion.description
|
||
|
||
@pytest.mark.unit
|
||
def test_calculate_strategy_price_premium(self):
|
||
"""测试高端款定价策略"""
|
||
service = PricingService(None)
|
||
|
||
suggestion = service.calculate_strategy_price(
|
||
base_cost=100.0,
|
||
strategy=StrategyType.PREMIUM,
|
||
)
|
||
|
||
# 高端款利润率 60%-80%,使用中间值 70%
|
||
# 价格 = 100 / (1 - 0.7) ≈ 333
|
||
assert suggestion.strategy == "高端款"
|
||
assert suggestion.suggested_price > 300
|
||
assert suggestion.margin > 60
|
||
assert "高端" in suggestion.description
|
||
|
||
@pytest.mark.unit
|
||
def test_calculate_strategy_price_with_market_reference(self):
|
||
"""测试带市场参考的定价"""
|
||
service = PricingService(None)
|
||
|
||
market_ref = MarketReference(min=80.0, max=150.0, avg=100.0)
|
||
|
||
# 引流款应该参考市场最低价
|
||
suggestion = service.calculate_strategy_price(
|
||
base_cost=50.0,
|
||
strategy=StrategyType.TRAFFIC,
|
||
market_ref=market_ref,
|
||
)
|
||
|
||
# 应该取市场最低价的 90% 和成本定价的较低者
|
||
assert suggestion.suggested_price <= 100 # 不会太高
|
||
assert suggestion.suggested_price >= 50 * 1.05 # 不低于成本
|
||
|
||
@pytest.mark.unit
|
||
def test_calculate_strategy_price_ensures_profit(self):
|
||
"""测试确保价格不低于成本"""
|
||
service = PricingService(None)
|
||
|
||
market_ref = MarketReference(min=30.0, max=50.0, avg=40.0)
|
||
|
||
# 即使市场价很低,也不能低于成本
|
||
suggestion = service.calculate_strategy_price(
|
||
base_cost=100.0, # 成本高于市场价
|
||
strategy=StrategyType.TRAFFIC,
|
||
market_ref=market_ref,
|
||
)
|
||
|
||
# 价格至少是成本的 1.05 倍
|
||
assert suggestion.suggested_price >= 100 * 1.05
|
||
|
||
@pytest.mark.unit
|
||
def test_calculate_all_strategies(self):
|
||
"""测试计算所有策略"""
|
||
service = PricingService(None)
|
||
|
||
suggestions = service.calculate_all_strategies(
|
||
base_cost=100.0,
|
||
target_margin=50.0,
|
||
)
|
||
|
||
assert suggestions.traffic is not None
|
||
assert suggestions.profit is not None
|
||
assert suggestions.premium is not None
|
||
|
||
# 价格应该递增:引流款 < 利润款 < 高端款
|
||
assert suggestions.traffic.suggested_price < suggestions.profit.suggested_price
|
||
assert suggestions.profit.suggested_price < suggestions.premium.suggested_price
|
||
|
||
@pytest.mark.unit
|
||
def test_calculate_all_strategies_selected(self):
|
||
"""测试只计算选定的策略"""
|
||
service = PricingService(None)
|
||
|
||
suggestions = service.calculate_all_strategies(
|
||
base_cost=100.0,
|
||
target_margin=50.0,
|
||
strategies=[StrategyType.TRAFFIC, StrategyType.PROFIT],
|
||
)
|
||
|
||
assert suggestions.traffic is not None
|
||
assert suggestions.profit is not None
|
||
assert suggestions.premium is None
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.asyncio
|
||
async def test_get_project_with_cost(
|
||
self,
|
||
db_session: AsyncSession,
|
||
sample_project_with_costs: Project
|
||
):
|
||
"""测试获取项目及成本"""
|
||
service = PricingService(db_session)
|
||
|
||
project, cost_summary = await service.get_project_with_cost(
|
||
sample_project_with_costs.id
|
||
)
|
||
|
||
assert project.id == sample_project_with_costs.id
|
||
assert project.project_name == "光子嫩肤"
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.asyncio
|
||
async def test_get_project_with_cost_not_found(
|
||
self,
|
||
db_session: AsyncSession
|
||
):
|
||
"""测试项目不存在时的错误处理"""
|
||
service = PricingService(db_session)
|
||
|
||
with pytest.raises(ValueError, match="项目不存在"):
|
||
await service.get_project_with_cost(99999)
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.asyncio
|
||
async def test_create_pricing_plan(
|
||
self,
|
||
db_session: AsyncSession,
|
||
sample_project: Project
|
||
):
|
||
"""测试创建定价方案"""
|
||
# 先添加成本汇总
|
||
cost_summary = ProjectCostSummary(
|
||
project_id=sample_project.id,
|
||
material_cost=Decimal("40.00"),
|
||
equipment_cost=Decimal("50.00"),
|
||
labor_cost=Decimal("60.00"),
|
||
fixed_cost_allocation=Decimal("30.00"),
|
||
total_cost=Decimal("180.00"),
|
||
calculated_at=datetime.now(),
|
||
)
|
||
db_session.add(cost_summary)
|
||
await db_session.commit()
|
||
|
||
service = PricingService(db_session)
|
||
|
||
plan = await service.create_pricing_plan(
|
||
project_id=sample_project.id,
|
||
plan_name="测试定价方案",
|
||
strategy_type=StrategyType.PROFIT,
|
||
target_margin=50.0,
|
||
)
|
||
|
||
assert plan.project_id == sample_project.id
|
||
assert plan.plan_name == "测试定价方案"
|
||
assert plan.strategy_type == "profit"
|
||
assert float(plan.target_margin) == 50.0
|
||
assert float(plan.base_cost) == 180.0
|
||
assert plan.suggested_price > plan.base_cost
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.asyncio
|
||
async def test_update_pricing_plan(
|
||
self,
|
||
db_session: AsyncSession,
|
||
sample_pricing_plan: PricingPlan
|
||
):
|
||
"""测试更新定价方案"""
|
||
service = PricingService(db_session)
|
||
|
||
updated = await service.update_pricing_plan(
|
||
plan_id=sample_pricing_plan.id,
|
||
final_price=599.00,
|
||
plan_name="更新后方案名",
|
||
)
|
||
|
||
assert float(updated.final_price) == 599.00
|
||
assert updated.plan_name == "更新后方案名"
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.asyncio
|
||
async def test_update_pricing_plan_not_found(
|
||
self,
|
||
db_session: AsyncSession
|
||
):
|
||
"""测试更新不存在的方案"""
|
||
service = PricingService(db_session)
|
||
|
||
with pytest.raises(ValueError, match="定价方案不存在"):
|
||
await service.update_pricing_plan(
|
||
plan_id=99999,
|
||
final_price=599.00,
|
||
)
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.asyncio
|
||
async def test_simulate_strategies(
|
||
self,
|
||
db_session: AsyncSession,
|
||
sample_project: Project
|
||
):
|
||
"""测试策略模拟"""
|
||
# 添加成本汇总
|
||
cost_summary = ProjectCostSummary(
|
||
project_id=sample_project.id,
|
||
total_cost=Decimal("200.00"),
|
||
material_cost=Decimal("100.00"),
|
||
equipment_cost=Decimal("50.00"),
|
||
labor_cost=Decimal("50.00"),
|
||
fixed_cost_allocation=Decimal("0.00"),
|
||
calculated_at=datetime.now(),
|
||
)
|
||
db_session.add(cost_summary)
|
||
await db_session.commit()
|
||
|
||
service = PricingService(db_session)
|
||
|
||
response = await service.simulate_strategies(
|
||
project_id=sample_project.id,
|
||
strategies=[StrategyType.TRAFFIC, StrategyType.PROFIT, StrategyType.PREMIUM],
|
||
target_margin=50.0,
|
||
)
|
||
|
||
assert response.project_id == sample_project.id
|
||
assert response.base_cost == 200.0
|
||
assert len(response.results) == 3
|
||
|
||
# 验证结果排序
|
||
prices = [r.suggested_price for r in response.results]
|
||
assert prices == sorted(prices) # 应该是升序
|
||
|
||
@pytest.mark.unit
|
||
def test_format_cost_data(self):
|
||
"""测试成本数据格式化"""
|
||
service = PricingService(None)
|
||
|
||
# 测试空数据
|
||
result = service._format_cost_data(None)
|
||
assert "暂无成本数据" in result
|
||
|
||
@pytest.mark.unit
|
||
def test_format_market_data(self):
|
||
"""测试市场数据格式化"""
|
||
service = PricingService(None)
|
||
|
||
# 测试空数据
|
||
result = service._format_market_data(None)
|
||
assert "暂无市场行情数据" in result
|
||
|
||
# 测试有数据
|
||
market_ref = MarketReference(min=100.0, max=500.0, avg=300.0)
|
||
result = service._format_market_data(market_ref)
|
||
assert "100.00" in result
|
||
assert "500.00" in result
|
||
assert "300.00" in result
|
||
|
||
@pytest.mark.unit
|
||
def test_extract_recommendations(self):
|
||
"""测试提取 AI 建议列表"""
|
||
service = PricingService(None)
|
||
|
||
content = """
|
||
根据分析,建议如下:
|
||
- 建议一:常规定价 580 元
|
||
- 建议二:新客首单 388 元
|
||
* 建议三:VIP 会员 520 元
|
||
1. 定期促销活动
|
||
2. 会员体系建设
|
||
"""
|
||
|
||
recommendations = service._extract_recommendations(content)
|
||
|
||
assert len(recommendations) == 5
|
||
assert "常规定价" in recommendations[0]
|
||
|
||
|
||
class TestPricingServiceWithAI:
|
||
"""需要 AI 服务的定价测试"""
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.asyncio
|
||
@patch('app.services.pricing_service.AIServiceWrapper')
|
||
async def test_generate_pricing_advice_ai_failure(
|
||
self,
|
||
mock_ai_wrapper,
|
||
db_session: AsyncSession,
|
||
sample_project: Project
|
||
):
|
||
"""测试 AI 调用失败时的降级处理"""
|
||
# 添加成本汇总
|
||
cost_summary = ProjectCostSummary(
|
||
project_id=sample_project.id,
|
||
total_cost=Decimal("200.00"),
|
||
material_cost=Decimal("100.00"),
|
||
equipment_cost=Decimal("50.00"),
|
||
labor_cost=Decimal("50.00"),
|
||
fixed_cost_allocation=Decimal("0.00"),
|
||
calculated_at=datetime.now(),
|
||
)
|
||
db_session.add(cost_summary)
|
||
await db_session.commit()
|
||
|
||
# 模拟 AI 调用失败
|
||
mock_instance = MagicMock()
|
||
mock_instance.chat = AsyncMock(side_effect=Exception("AI 服务不可用"))
|
||
mock_ai_wrapper.return_value = mock_instance
|
||
|
||
service = PricingService(db_session)
|
||
|
||
# 即使 AI 失败,基本定价计算应该仍然返回
|
||
response = await service.generate_pricing_advice(
|
||
project_id=sample_project.id,
|
||
target_margin=50.0,
|
||
)
|
||
|
||
# 验证基本定价仍然可用
|
||
assert response.project_id == sample_project.id
|
||
assert response.cost_base == 200.0
|
||
assert response.pricing_suggestions is not None
|
||
# AI 建议可能为空
|
||
assert response.ai_advice is None or response.ai_usage is None
|