296 lines
11 KiB
Python
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
|