Files
smart-project-pricing/后端服务/tests/test_services/test_cost_service.py
2026-01-31 21:33:06 +08:00

416 lines
13 KiB
Python

"""成本计算服务单元测试
测试 CostService 的核心业务逻辑
"""
import pytest
from decimal import Decimal
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.cost_service import CostService
from app.schemas.project_cost import AllocationMethod, CostItemType
from app.models import (
Material, Equipment, StaffLevel, Project, FixedCost,
ProjectCostItem, ProjectLaborCost, ProjectCostSummary
)
class TestCostService:
"""成本服务测试类"""
@pytest.mark.unit
@pytest.mark.asyncio
async def test_get_material_info(
self,
db_session: AsyncSession,
sample_material: Material
):
"""测试获取耗材信息"""
service = CostService(db_session)
# 获取存在的耗材
material = await service.get_material_info(sample_material.id)
assert material is not None
assert material.material_name == "冷凝胶"
assert material.unit_price == Decimal("2.00")
# 获取不存在的耗材
material = await service.get_material_info(99999)
assert material is None
@pytest.mark.unit
@pytest.mark.asyncio
async def test_get_equipment_info(
self,
db_session: AsyncSession,
sample_equipment: Equipment
):
"""测试获取设备信息"""
service = CostService(db_session)
# 获取存在的设备
equipment = await service.get_equipment_info(sample_equipment.id)
assert equipment is not None
assert equipment.equipment_name == "光子仪"
assert equipment.depreciation_per_use == Decimal("47.50")
# 获取不存在的设备
equipment = await service.get_equipment_info(99999)
assert equipment is None
@pytest.mark.unit
@pytest.mark.asyncio
async def test_get_staff_level_info(
self,
db_session: AsyncSession,
sample_staff_level: StaffLevel
):
"""测试获取人员级别信息"""
service = CostService(db_session)
# 获取存在的级别
level = await service.get_staff_level_info(sample_staff_level.id)
assert level is not None
assert level.level_name == "中级美容师"
assert level.hourly_rate == Decimal("50.00")
# 获取不存在的级别
level = await service.get_staff_level_info(99999)
assert level is None
@pytest.mark.unit
@pytest.mark.asyncio
async def test_calculate_material_cost(
self,
db_session: AsyncSession,
sample_project_with_costs: Project
):
"""测试耗材成本计算"""
service = CostService(db_session)
total, breakdown = await service.calculate_material_cost(
sample_project_with_costs.id
)
assert total == Decimal("40.00") # 20 * 2.00
assert len(breakdown) == 1
assert breakdown[0]["name"] == "冷凝胶"
assert breakdown[0]["quantity"] == 20.0
assert breakdown[0]["total"] == 40.0
@pytest.mark.unit
@pytest.mark.asyncio
async def test_calculate_equipment_cost(
self,
db_session: AsyncSession,
sample_project_with_costs: Project
):
"""测试设备折旧成本计算"""
service = CostService(db_session)
total, breakdown = await service.calculate_equipment_cost(
sample_project_with_costs.id
)
assert total == Decimal("47.50") # 1 * 47.50
assert len(breakdown) == 1
assert breakdown[0]["name"] == "光子仪"
assert breakdown[0]["depreciation_per_use"] == 47.5
@pytest.mark.unit
@pytest.mark.asyncio
async def test_calculate_labor_cost(
self,
db_session: AsyncSession,
sample_project_with_costs: Project
):
"""测试人工成本计算"""
service = CostService(db_session)
total, breakdown = await service.calculate_labor_cost(
sample_project_with_costs.id
)
assert total == Decimal("50.00") # 60分钟 / 60 * 50
assert len(breakdown) == 1
assert breakdown[0]["name"] == "中级美容师"
assert breakdown[0]["duration_minutes"] == 60
assert breakdown[0]["hourly_rate"] == 50.0
@pytest.mark.unit
@pytest.mark.asyncio
async def test_calculate_fixed_cost_allocation_by_count(
self,
db_session: AsyncSession,
sample_project: Project,
sample_fixed_cost: FixedCost
):
"""测试固定成本按项目数量分摊"""
service = CostService(db_session)
allocation, detail = await service.calculate_fixed_cost_allocation(
sample_project.id,
method=AllocationMethod.COUNT
)
# 只有一个项目,分摊全部固定成本
assert allocation == Decimal("30000.00")
assert detail["method"] == "count"
assert detail["project_count"] == 1
@pytest.mark.unit
@pytest.mark.asyncio
async def test_calculate_fixed_cost_allocation_by_duration(
self,
db_session: AsyncSession,
sample_project: Project,
sample_fixed_cost: FixedCost
):
"""测试固定成本按时长分摊"""
service = CostService(db_session)
allocation, detail = await service.calculate_fixed_cost_allocation(
sample_project.id,
method=AllocationMethod.DURATION
)
# 只有一个项目,占比 100%
assert allocation == Decimal("30000.00")
assert detail["method"] == "duration"
assert detail["project_duration"] == 60
@pytest.mark.unit
@pytest.mark.asyncio
async def test_calculate_project_cost(
self,
db_session: AsyncSession,
sample_project_with_costs: Project,
sample_fixed_cost: FixedCost
):
"""测试项目总成本计算"""
service = CostService(db_session)
result = await service.calculate_project_cost(
sample_project_with_costs.id,
allocation_method=AllocationMethod.COUNT
)
assert result.project_id == sample_project_with_costs.id
assert result.project_name == "光子嫩肤"
# 验证成本构成
breakdown = result.cost_breakdown
assert breakdown["material_cost"]["subtotal"] == 40.0
assert breakdown["equipment_cost"]["subtotal"] == 47.5
assert breakdown["labor_cost"]["subtotal"] == 50.0
# 总成本 = 耗材40 + 设备47.5 + 人工50 + 固定30000
expected_total = 40 + 47.5 + 50 + 30000
assert result.total_cost == expected_total
assert result.min_price_suggestion == expected_total
@pytest.mark.unit
@pytest.mark.asyncio
async def test_calculate_project_cost_not_found(
self,
db_session: AsyncSession
):
"""测试项目不存在时的错误处理"""
service = CostService(db_session)
with pytest.raises(ValueError, match="项目不存在"):
await service.calculate_project_cost(99999)
@pytest.mark.unit
@pytest.mark.asyncio
async def test_add_cost_item_material(
self,
db_session: AsyncSession,
sample_project: Project,
sample_material: Material
):
"""测试添加耗材成本明细"""
service = CostService(db_session)
cost_item = await service.add_cost_item(
project_id=sample_project.id,
item_type=CostItemType.MATERIAL,
item_id=sample_material.id,
quantity=10,
remark="测试备注"
)
assert cost_item.project_id == sample_project.id
assert cost_item.item_type == "material"
assert cost_item.quantity == Decimal("10")
assert cost_item.unit_cost == Decimal("2.00")
assert cost_item.total_cost == Decimal("20.00")
assert cost_item.remark == "测试备注"
@pytest.mark.unit
@pytest.mark.asyncio
async def test_add_cost_item_equipment(
self,
db_session: AsyncSession,
sample_project: Project,
sample_equipment: Equipment
):
"""测试添加设备折旧成本明细"""
service = CostService(db_session)
cost_item = await service.add_cost_item(
project_id=sample_project.id,
item_type=CostItemType.EQUIPMENT,
item_id=sample_equipment.id,
quantity=1,
)
assert cost_item.item_type == "equipment"
assert cost_item.unit_cost == Decimal("47.50")
assert cost_item.total_cost == Decimal("47.50")
@pytest.mark.unit
@pytest.mark.asyncio
async def test_add_cost_item_not_found(
self,
db_session: AsyncSession,
sample_project: Project
):
"""测试添加不存在的耗材/设备时的错误处理"""
service = CostService(db_session)
with pytest.raises(ValueError, match="耗材不存在"):
await service.add_cost_item(
project_id=sample_project.id,
item_type=CostItemType.MATERIAL,
item_id=99999,
quantity=1,
)
with pytest.raises(ValueError, match="设备不存在"):
await service.add_cost_item(
project_id=sample_project.id,
item_type=CostItemType.EQUIPMENT,
item_id=99999,
quantity=1,
)
@pytest.mark.unit
@pytest.mark.asyncio
async def test_add_labor_cost(
self,
db_session: AsyncSession,
sample_project: Project,
sample_staff_level: StaffLevel
):
"""测试添加人工成本"""
service = CostService(db_session)
labor_cost = await service.add_labor_cost(
project_id=sample_project.id,
staff_level_id=sample_staff_level.id,
duration_minutes=30,
remark="测试人工"
)
assert labor_cost.project_id == sample_project.id
assert labor_cost.duration_minutes == 30
assert labor_cost.hourly_rate == Decimal("50.00")
# 30分钟 / 60 * 50 = 25
assert labor_cost.labor_cost == Decimal("25.00")
@pytest.mark.unit
@pytest.mark.asyncio
async def test_add_labor_cost_not_found(
self,
db_session: AsyncSession,
sample_project: Project
):
"""测试添加不存在的人员级别时的错误处理"""
service = CostService(db_session)
with pytest.raises(ValueError, match="人员级别不存在"):
await service.add_labor_cost(
project_id=sample_project.id,
staff_level_id=99999,
duration_minutes=30,
)
@pytest.mark.unit
@pytest.mark.asyncio
async def test_update_cost_item(
self,
db_session: AsyncSession,
sample_project: Project,
sample_material: Material
):
"""测试更新成本明细"""
service = CostService(db_session)
# 先添加
cost_item = await service.add_cost_item(
project_id=sample_project.id,
item_type=CostItemType.MATERIAL,
item_id=sample_material.id,
quantity=10,
)
# 更新数量
updated = await service.update_cost_item(
cost_item=cost_item,
quantity=20,
remark="更新后备注"
)
assert updated.quantity == Decimal("20")
assert updated.total_cost == Decimal("40.00") # 20 * 2
assert updated.remark == "更新后备注"
@pytest.mark.unit
@pytest.mark.asyncio
async def test_update_labor_cost(
self,
db_session: AsyncSession,
sample_project: Project,
sample_staff_level: StaffLevel
):
"""测试更新人工成本"""
service = CostService(db_session)
# 先添加
labor = await service.add_labor_cost(
project_id=sample_project.id,
staff_level_id=sample_staff_level.id,
duration_minutes=30,
)
# 更新时长
updated = await service.update_labor_cost(
labor_item=labor,
duration_minutes=60,
)
assert updated.duration_minutes == 60
assert updated.labor_cost == Decimal("50.00") # 60/60 * 50
@pytest.mark.unit
@pytest.mark.asyncio
async def test_empty_project_cost(
self,
db_session: AsyncSession,
sample_project: Project
):
"""测试没有成本明细的项目计算"""
service = CostService(db_session)
# 计算空项目成本(无固定成本)
total_material, _ = await service.calculate_material_cost(sample_project.id)
total_equipment, _ = await service.calculate_equipment_cost(sample_project.id)
total_labor, _ = await service.calculate_labor_cost(sample_project.id)
assert total_material == Decimal("0")
assert total_equipment == Decimal("0")
assert total_labor == Decimal("0")