"""智能定价服务单元测试 测试 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