"""定价引擎扩展测试 — 补充重量/按件/按米定价、阈值附加费。 测试用例: - 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