dingdanquanliucheng/backend/tests/test_pricing_extended.py
2026-06-19 23:03:14 +08:00

296 lines
11 KiB
Python

"""定价引擎扩展测试 — 补充重量/按件/按米定价、阈值附加费。
测试用例:
- PRICE-002: 重量定价规则创建 (pricing_type=kg)
- PRICE-011: 重量报价计算
- PRICE-015: 阈值附加费 (per_sqm_threshold)
- 按件定价 (pricing_type=unit)
- 按米定价 (pricing_type=linear_m)
"""
from __future__ import annotations
import json
import pytest
@pytest.mark.pricing
class TestWeightPricing:
"""PRICE-002, PRICE-011: 重量定价测试。"""
def _make_rule(self, **overrides):
class FakeRule:
pass
rule = FakeRule()
rule.base_unit_price = overrides.get("base_unit_price", 8.0)
rule.pricing_type = overrides.get("pricing_type", "kg")
rule.pricing_unit = overrides.get("pricing_unit", "kg")
rule.pricing_inputs = overrides.get("pricing_inputs", json.dumps([
{"key": "weight", "type": "number", "default_unit": "kg"},
]))
rule.formula_expr = overrides.get("formula_expr", "$input.weight * $rule.base_unit_price")
rule.formula_constants = overrides.get("formula_constants", None)
rule.surcharge_json = overrides.get("surcharge_json", None)
rule.formula_note = overrides.get("formula_note", None)
rule.tax_rate = overrides.get("tax_rate", 0)
rule.tax_inclusive = overrides.get("tax_inclusive", 0)
return rule
def test_weight_pricing_basic(self):
"""PRICE-011: 重量报价计算, 5kg x ¥8/kg = ¥40。"""
from backend.app.services.pricing_engine import pricing_engine
rule = self._make_rule(base_unit_price=8.0)
result = pricing_engine.calculate(rule, {"weight": 5})
assert result["base_cost"] == 40.0
assert result["cost_price"] == 40.0
def test_weight_pricing_with_surcharge(self):
"""重量定价 + 附加费。"""
from backend.app.services.pricing_engine import pricing_engine
rule = self._make_rule(
base_unit_price=8.0,
surcharge_json=json.dumps({
"pack": {"name": "包装费", "method": "per_piece", "price": 2, "input_key": "qty"}
}),
)
result = pricing_engine.calculate(rule, {"weight": 5, "qty": 10, "surcharge_pack": True})
assert result["base_cost"] == 40.0
assert result["total_surcharge"] == 20.0
assert result["cost_price"] == 60.0
def test_weight_pricing_zero(self):
"""重量为0返回0。"""
from backend.app.services.pricing_engine import pricing_engine
rule = self._make_rule()
result = pricing_engine.calculate(rule, {"weight": 0})
assert result["base_cost"] == 0.0
@pytest.mark.pricing
class TestUnitPricing:
"""按件定价测试。"""
def _make_rule(self, **overrides):
class FakeRule:
pass
rule = FakeRule()
rule.base_unit_price = overrides.get("base_unit_price", 25.0)
rule.pricing_type = "unit"
rule.pricing_unit = ""
rule.pricing_inputs = overrides.get("pricing_inputs", json.dumps([
{"key": "qty", "type": "number"},
]))
rule.formula_expr = overrides.get("formula_expr", "$input.qty * $rule.base_unit_price")
rule.formula_constants = None
rule.surcharge_json = None
rule.formula_note = None
rule.tax_rate = 0
rule.tax_inclusive = 0
return rule
def test_unit_pricing_basic(self):
"""按件定价, 10件 x ¥25/件 = ¥250。"""
from backend.app.services.pricing_engine import pricing_engine
rule = self._make_rule()
result = pricing_engine.calculate(rule, {"qty": 10})
assert result["base_cost"] == 250.0
def test_unit_pricing_with_tax(self):
"""按件定价 + 不含税。"""
from backend.app.services.pricing_engine import pricing_engine
rule = self._make_rule(base_unit_price=25.0)
rule.tax_rate = 13
rule.tax_inclusive = 0
result = pricing_engine.calculate(rule, {"qty": 10})
assert result["base_cost"] == 250.0
assert result["tax_amount"] == 32.5
assert result["price_in_tax"] == 282.5
@pytest.mark.pricing
class TestLinearMPricing:
"""按米定价测试。"""
def _make_rule(self, **overrides):
class FakeRule:
pass
rule = FakeRule()
rule.base_unit_price = overrides.get("base_unit_price", 15.0)
rule.pricing_type = "linear_m"
rule.pricing_unit = "m"
rule.pricing_inputs = overrides.get("pricing_inputs", json.dumps([
{"key": "length", "type": "number", "default_unit": "m"},
]))
rule.formula_expr = overrides.get("formula_expr", "$input.length_m * $rule.base_unit_price")
rule.formula_constants = None
rule.surcharge_json = None
rule.formula_note = None
rule.tax_rate = 0
rule.tax_inclusive = 0
return rule
def test_linear_m_pricing_basic(self):
"""按米定价, 20m x ¥15/m = ¥300。"""
from backend.app.services.pricing_engine import pricing_engine
rule = self._make_rule()
result = pricing_engine.calculate(rule, {"length": 20})
assert result["base_cost"] == 300.0
def test_linear_m_pricing_cm(self):
"""按米定价, cm单位换算, 2000cm x ¥15/m = ¥300。"""
from backend.app.services.pricing_engine import pricing_engine
rule = self._make_rule()
result = pricing_engine.calculate(rule, {"length": 2000, "length_unit": "cm"})
assert result["base_cost"] == 300.0
@pytest.mark.pricing
class TestThresholdSurcharge:
"""PRICE-015: 阈值附加费测试。"""
def test_threshold_surcharge_below(self):
"""阈值附加费 — 属性值 <= 阈值, 使用 price_true。"""
from backend.app.services.pricing_engine import pricing_engine
class FakeRule:
pass
rule = FakeRule()
rule.base_unit_price = 50.0
rule.pricing_type = "area"
rule.pricing_unit = ""
rule.pricing_inputs = json.dumps([
{"key": "length", "type": "number", "default_unit": "m"},
{"key": "width", "type": "number", "default_unit": "m"},
])
rule.formula_expr = "$input.length_m * $input.width_m * $rule.base_unit_price"
rule.formula_constants = None
rule.surcharge_json = json.dumps({
"coating": {
"name": "涂层费",
"method": "per_sqm_threshold",
"threshold": {
"field": "thickness",
"lte": 0.05,
"price_true": 5,
"price_false": 10,
}
}
})
rule.formula_note = None
rule.tax_rate = 0
rule.tax_inclusive = 0
# thickness=0.03 <= 0.05, 使用 price_true=5
result = pricing_engine.calculate(
rule,
{"length": 2, "width": 1, "surcharge_coating": True},
product_attrs={"thickness": 0.03},
)
# 面积=2㎡, 附加费=2*5=10
assert result["total_surcharge"] == 10.0
def test_threshold_surcharge_above(self):
"""阈值附加费 — 属性值 > 阈值, 使用 price_false。"""
from backend.app.services.pricing_engine import pricing_engine
class FakeRule:
pass
rule = FakeRule()
rule.base_unit_price = 50.0
rule.pricing_type = "area"
rule.pricing_unit = ""
rule.pricing_inputs = json.dumps([
{"key": "length", "type": "number", "default_unit": "m"},
{"key": "width", "type": "number", "default_unit": "m"},
])
rule.formula_expr = "$input.length_m * $input.width_m * $rule.base_unit_price"
rule.formula_constants = None
rule.surcharge_json = json.dumps({
"coating": {
"name": "涂层费",
"method": "per_sqm_threshold",
"threshold": {
"field": "thickness",
"lte": 0.05,
"price_true": 5,
"price_false": 10,
}
}
})
rule.formula_note = None
rule.tax_rate = 0
rule.tax_inclusive = 0
# thickness=0.08 > 0.05, 使用 price_false=10
result = pricing_engine.calculate(
rule,
{"length": 2, "width": 1, "surcharge_coating": True},
product_attrs={"thickness": 0.08},
)
# 面积=2㎡, 附加费=2*10=20
assert result["total_surcharge"] == 20.0
@pytest.mark.pricing
@pytest.mark.p1
class TestPricingRuleCRUDExtended:
"""定价规则 CRUD 扩展。"""
def test_create_weight_pricing_rule(self, client, admin_headers, make_product):
"""PRICE-002: 创建重量定价规则。"""
product = make_product()
resp = client.post("/api/pricing-rules", headers=admin_headers, json={
"product_id": product.id,
"product_name": product.product_name,
"pricing_type": "kg",
"base_unit_price": 8.0,
"pricing_inputs": json.dumps([{"key": "weight", "type": "number"}]),
"formula_expr": "$input.weight * $rule.base_unit_price",
})
assert resp.status_code == 200
assert resp.json()["data"]["pricing_type"] == "kg"
def test_create_unit_pricing_rule(self, client, admin_headers, make_product):
"""创建按件定价规则。"""
product = make_product()
resp = client.post("/api/pricing-rules", headers=admin_headers, json={
"product_id": product.id,
"product_name": product.product_name,
"pricing_type": "unit",
"base_unit_price": 25.0,
"pricing_inputs": json.dumps([{"key": "qty", "type": "number"}]),
"formula_expr": "$input.qty * $rule.base_unit_price",
})
assert resp.status_code == 200
assert resp.json()["data"]["pricing_type"] == "unit"
def test_create_linear_m_pricing_rule(self, client, admin_headers, make_product):
"""创建按米定价规则。"""
product = make_product()
resp = client.post("/api/pricing-rules", headers=admin_headers, json={
"product_id": product.id,
"product_name": product.product_name,
"pricing_type": "linear_m",
"base_unit_price": 15.0,
"pricing_inputs": json.dumps([{"key": "length", "type": "number", "default_unit": "m"}]),
"formula_expr": "$input.length_m * $rule.base_unit_price",
})
assert resp.status_code == 200
assert resp.json()["data"]["pricing_type"] == "linear_m"
def test_update_pricing_rule_tax(self, client, admin_headers, make_product):
"""更新定价规则的税率。"""
product = make_product()
create_resp = client.post("/api/pricing-rules", headers=admin_headers, json={
"product_id": product.id,
"product_name": product.product_name,
"pricing_type": "area",
"base_unit_price": 50.0,
})
rule_id = create_resp.json()["data"]["id"]
resp = client.put(f"/api/pricing-rules/{rule_id}", headers=admin_headers, json={
"tax_rate": 13.0,
"tax_inclusive": 0,
})
assert resp.status_code == 200
assert resp.json()["data"]["tax_rate"] == 13.0