667 lines
30 KiB
Python
667 lines
30 KiB
Python
"""定价引擎测试 — 覆盖报价计算、定价规则CRUD、供应商成本、价格层级。
|
||
|
||
测试用例:
|
||
- PRICE-001 ~ PRICE-007: 定价规则 CRUD
|
||
- PRICE-010 ~ PRICE-020: 报价计算(面积/重量/按件/按米、附加费、税、条件公式)
|
||
- PRICE-030 ~ PRICE-033: 供应商成本 CRUD
|
||
- PRICE-040 ~ PRICE-043: 价格层级 CRUD
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import pytest
|
||
|
||
|
||
# ============================================================
|
||
# 定价引擎单元测试(直接调用 PricingEngine,不走 API)
|
||
# ============================================================
|
||
|
||
@pytest.mark.pricing
|
||
class TestPricingEngineUnit:
|
||
"""定价引擎核心计算逻辑的单元测试。"""
|
||
|
||
def _make_rule(self, **overrides):
|
||
"""创建模拟定价规则对象。"""
|
||
class FakeRule:
|
||
pass
|
||
rule = FakeRule()
|
||
rule.base_unit_price = overrides.get("base_unit_price", 50.0)
|
||
rule.pricing_type = overrides.get("pricing_type", "area")
|
||
rule.pricing_unit = overrides.get("pricing_unit", "㎡")
|
||
rule.pricing_inputs = overrides.get("pricing_inputs", json.dumps([
|
||
{"key": "length", "type": "number", "default_unit": "m"},
|
||
{"key": "width", "type": "number", "default_unit": "m"},
|
||
]))
|
||
rule.formula_expr = overrides.get("formula_expr", "$input.length_m * $input.width_m * $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_area_pricing_basic(self):
|
||
"""PRICE-010: 面积定价 — 2m x 1.2m x ¥50/㎡ = ¥120"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(base_unit_price=50.0)
|
||
result = pricing_engine.calculate(rule, {"length": 2, "width": 1.2})
|
||
assert result["base_cost"] == 120.0
|
||
assert result["cost_price"] == 120.0
|
||
assert result["total_surcharge"] == 0
|
||
|
||
def test_area_pricing_cm_unit(self):
|
||
"""单位换算: cm → m, 200cm x 120cm x ¥50/㎡ = ¥120"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(base_unit_price=50.0)
|
||
result = pricing_engine.calculate(rule, {
|
||
"length": 200, "length_unit": "cm",
|
||
"width": 120, "width_unit": "cm",
|
||
})
|
||
assert result["base_cost"] == 120.0
|
||
|
||
def test_area_pricing_mm_unit(self):
|
||
"""单位换算: mm → m, 2000mm x 1200mm x ¥50/㎡ = ¥120"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(base_unit_price=50.0)
|
||
result = pricing_engine.calculate(rule, {
|
||
"length": 2000, "length_unit": "mm",
|
||
"width": 1200, "width_unit": "mm",
|
||
})
|
||
assert result["base_cost"] == 120.0
|
||
|
||
def test_formula_with_constants(self):
|
||
"""PRICE-019: 含常量的公式计算, $const.rate 作为乘数"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(
|
||
base_unit_price=50.0,
|
||
formula_expr="$input.length_m * $input.width_m * $rule.base_unit_price * $const.rate",
|
||
formula_constants=json.dumps({"rate": 1.5}),
|
||
)
|
||
# 1*1*50*1.5 = 75
|
||
result = pricing_engine.calculate(rule, {"length": 1, "width": 1})
|
||
assert result["base_cost"] == 75.0
|
||
|
||
def test_surcharge_per_sqm(self):
|
||
"""PRICE-012: 按面积附加费, 2m*1.2m=2.4㎡, ¥10/㎡ = ¥24"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(
|
||
base_unit_price=50.0,
|
||
surcharge_json=json.dumps({
|
||
"urgent": {"name": "加急费", "method": "per_sqm", "price": 10}
|
||
}),
|
||
)
|
||
result = pricing_engine.calculate(rule, {
|
||
"length": 2, "width": 1.2, "surcharge_urgent": True,
|
||
})
|
||
assert result["total_surcharge"] == 24.0
|
||
assert len(result["surcharge_items"]) == 1
|
||
assert result["surcharge_items"][0]["key"] == "urgent"
|
||
|
||
def test_surcharge_per_piece(self):
|
||
"""PRICE-013: 按件附加费, 10件 x ¥5/件 = ¥50"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(
|
||
base_unit_price=50.0,
|
||
surcharge_json=json.dumps({
|
||
"packaging": {"name": "包装费", "method": "per_piece", "price": 5, "input_key": "qty"}
|
||
}),
|
||
)
|
||
result = pricing_engine.calculate(rule, {
|
||
"length": 2, "width": 1.2, "qty": 10, "surcharge_packaging": True,
|
||
})
|
||
assert result["total_surcharge"] == 50.0
|
||
|
||
def test_surcharge_per_linear_m(self):
|
||
"""PRICE-014: 按米附加费, 20m x ¥3/m = ¥60"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(
|
||
base_unit_price=50.0,
|
||
surcharge_json=json.dumps({
|
||
"edge": {"name": "包边费", "method": "per_linear_m", "price": 3, "input_key": "edge_length"}
|
||
}),
|
||
)
|
||
result = pricing_engine.calculate(rule, {
|
||
"length": 2, "width": 1.2, "edge_length": 20, "surcharge_edge": True,
|
||
})
|
||
assert result["total_surcharge"] == 60.0
|
||
|
||
def test_surcharge_not_enabled(self):
|
||
"""未启用的附加费不计入"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(
|
||
base_unit_price=50.0,
|
||
surcharge_json=json.dumps({
|
||
"urgent": {"name": "加急费", "method": "per_sqm", "price": 10}
|
||
}),
|
||
)
|
||
result = pricing_engine.calculate(rule, {"length": 2, "width": 1.2})
|
||
assert result["total_surcharge"] == 0
|
||
assert len(result["surcharge_items"]) == 0
|
||
|
||
def test_tax_exclusive(self):
|
||
"""PRICE-017: 不含税定价, cost=100, tax_rate=13%, tax_amount=13, price_in_tax=113"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(base_unit_price=50.0, tax_rate=13, tax_inclusive=0)
|
||
# 2m x 1m x 50 = 100
|
||
result = pricing_engine.calculate(rule, {"length": 2, "width": 1})
|
||
assert result["cost_price"] == 100.0
|
||
assert result["tax_rate"] == 13
|
||
assert result["price_ex_tax"] == 100.0
|
||
assert result["tax_amount"] == 13.0
|
||
assert result["price_in_tax"] == 113.0
|
||
|
||
def test_tax_inclusive(self):
|
||
"""PRICE-016: 含税定价, cost=113(含税), tax_rate=13%, price_ex_tax≈100"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(base_unit_price=56.5, tax_rate=13, tax_inclusive=1)
|
||
# 2m x 1m x 56.5 = 113
|
||
result = pricing_engine.calculate(rule, {"length": 2, "width": 1})
|
||
assert result["cost_price"] == 113.0
|
||
assert result["price_in_tax"] == 113.0
|
||
assert result["price_ex_tax"] == 100.0
|
||
assert result["tax_amount"] == 13.0
|
||
|
||
def test_no_tax(self):
|
||
"""无税率时, tax_amount=0, price_in_tax=cost_price"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(base_unit_price=50.0, tax_rate=0)
|
||
result = pricing_engine.calculate(rule, {"length": 2, "width": 1})
|
||
assert result["tax_amount"] == 0
|
||
assert result["price_in_tax"] == result["cost_price"]
|
||
|
||
def test_empty_inputs(self):
|
||
"""空输入返回 0"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule()
|
||
result = pricing_engine.calculate(rule, {})
|
||
assert result["base_cost"] == 0.0
|
||
|
||
def test_invalid_formula_returns_zero(self):
|
||
"""无效公式返回 0, 不抛异常"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(formula_expr="$nonexistent.field * ???")
|
||
result = pricing_engine.calculate(rule, {"length": 2, "width": 1})
|
||
assert result["base_cost"] == 0.0
|
||
|
||
def test_formula_detail_text(self):
|
||
"""公式说明包含关键信息"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(base_unit_price=50.0)
|
||
result = pricing_engine.calculate(rule, {"length": 2, "width": 1.2})
|
||
assert "2.0m" in result["formula_detail"]
|
||
assert "1.2m" in result["formula_detail"]
|
||
assert "120.00" in result["formula_detail"]
|
||
|
||
def test_formula_detail_with_surcharge(self):
|
||
"""公式说明包含附加费项"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(
|
||
base_unit_price=50.0,
|
||
surcharge_json=json.dumps({
|
||
"urgent": {"name": "加急费", "method": "per_sqm", "price": 10}
|
||
}),
|
||
)
|
||
result = pricing_engine.calculate(rule, {
|
||
"length": 2, "width": 1.2, "surcharge_urgent": True,
|
||
})
|
||
assert "加急费" in result["formula_detail"]
|
||
|
||
def test_formula_detail_with_tax(self):
|
||
"""公式说明包含税率信息"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(base_unit_price=50.0, tax_rate=13, tax_inclusive=0)
|
||
result = pricing_engine.calculate(rule, {"length": 2, "width": 1})
|
||
assert "13" in result["formula_detail"]
|
||
assert "税" in result["formula_detail"]
|
||
|
||
def test_get_surcharge_options(self):
|
||
"""附加费选项列表"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(surcharge_json=json.dumps({
|
||
"urgent": {"name": "加急费", "method": "per_sqm", "price": 10},
|
||
"pack": {"name": "包装费", "method": "per_piece", "price": 5, "input_key": "qty"},
|
||
}))
|
||
options = pricing_engine.get_available_surcharge_options(rule)
|
||
assert len(options) == 2
|
||
assert options[0]["key"] == "urgent"
|
||
assert options[1]["key"] == "pack"
|
||
|
||
def test_conditional_formula(self):
|
||
"""条件公式: ${1 > 0} ? 100 : 50 → 100"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(
|
||
formula_expr="${$input.length_m > 1} ? $input.length_m * $rule.base_unit_price : $rule.base_unit_price",
|
||
)
|
||
result = pricing_engine.calculate(rule, {"length": 2, "width": 1})
|
||
assert result["base_cost"] == 100.0
|
||
|
||
def test_conditional_formula_false_branch(self):
|
||
"""条件公式假分支: length=0.5 <= 1, 走 50"""
|
||
from backend.app.services.pricing_engine import pricing_engine
|
||
rule = self._make_rule(
|
||
formula_expr="${$input.length_m > 1} ? $input.length_m * $rule.base_unit_price : $rule.base_unit_price",
|
||
)
|
||
result = pricing_engine.calculate(rule, {"length": 0.5, "width": 1})
|
||
assert result["base_cost"] == 50.0
|
||
|
||
|
||
# ============================================================
|
||
# 定价规则 CRUD API 测试
|
||
# ============================================================
|
||
|
||
@pytest.mark.pricing
|
||
@pytest.mark.p1
|
||
class TestPricingRuleCRUD:
|
||
"""PRICE-001 ~ PRICE-007: 定价规则 API 测试。"""
|
||
|
||
def _create_rule(self, client, headers, product_id=1, **overrides):
|
||
"""辅助: 创建定价规则。"""
|
||
payload = {
|
||
"product_id": product_id,
|
||
"product_name": f"产品{product_id}",
|
||
"pricing_type": "area",
|
||
"base_unit_price": 50.0,
|
||
"pricing_inputs": json.dumps([
|
||
{"key": "length", "type": "number", "default_unit": "m"},
|
||
{"key": "width", "type": "number", "default_unit": "m"},
|
||
]),
|
||
"formula_expr": "$input.length_m * $input.width_m * $rule.base_unit_price",
|
||
}
|
||
payload.update(overrides)
|
||
resp = client.post("/api/pricing-rules", headers=headers, json=payload)
|
||
return resp
|
||
|
||
def test_create_pricing_rule(self, client, admin_headers, make_product):
|
||
"""PRICE-001: 创建面积定价规则。"""
|
||
product = make_product()
|
||
resp = self._create_rule(client, admin_headers, product_id=product.id)
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["code"] == 0
|
||
assert data["data"]["product_id"] == product.id
|
||
assert data["data"]["pricing_type"] == "area"
|
||
assert data["data"]["base_unit_price"] == 50.0
|
||
|
||
def test_create_duplicate_rule_rejected(self, client, admin_headers, make_product):
|
||
"""PRICE-001b: 同产品重复创建应拒绝。"""
|
||
product = make_product()
|
||
self._create_rule(client, admin_headers, product_id=product.id)
|
||
resp = self._create_rule(client, admin_headers, product_id=product.id)
|
||
assert resp.status_code == 400
|
||
|
||
def test_list_pricing_rules(self, client, admin_headers, make_product):
|
||
"""PRICE-007: 查询定价规则列表。"""
|
||
product = make_product()
|
||
self._create_rule(client, admin_headers, product_id=product.id)
|
||
resp = client.get("/api/pricing-rules", headers=admin_headers)
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["data"]["total"] >= 1
|
||
|
||
def test_list_pricing_rules_by_product(self, client, admin_headers, make_product):
|
||
"""按产品ID筛选定价规则。"""
|
||
product = make_product()
|
||
self._create_rule(client, admin_headers, product_id=product.id)
|
||
resp = client.get(f"/api/pricing-rules?product_id={product.id}", headers=admin_headers)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["data"]["total"] >= 1
|
||
|
||
def test_get_pricing_rule(self, client, admin_headers, make_product):
|
||
"""按产品ID获取定价规则。"""
|
||
product = make_product()
|
||
self._create_rule(client, admin_headers, product_id=product.id)
|
||
resp = client.get(f"/api/pricing-rules/{product.id}", headers=admin_headers)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["data"]["product_id"] == product.id
|
||
|
||
def test_get_pricing_rule_not_found(self, client, admin_headers):
|
||
"""查询不存在的产品定价规则返回 404。"""
|
||
resp = client.get("/api/pricing-rules/99999", headers=admin_headers)
|
||
assert resp.status_code == 404
|
||
|
||
def test_update_pricing_rule(self, client, admin_headers, make_product):
|
||
"""PRICE-005: 更新定价规则。"""
|
||
product = make_product()
|
||
create_resp = self._create_rule(client, admin_headers, product_id=product.id)
|
||
rule_id = create_resp.json()["data"]["id"]
|
||
resp = client.put(f"/api/pricing-rules/{rule_id}", headers=admin_headers, json={
|
||
"base_unit_price": 80.0,
|
||
})
|
||
assert resp.status_code == 200
|
||
assert resp.json()["data"]["base_unit_price"] == 80.0
|
||
|
||
def test_update_pricing_rule_not_found(self, client, admin_headers):
|
||
"""更新不存在的定价规则返回 404。"""
|
||
resp = client.put("/api/pricing-rules/99999", headers=admin_headers, json={
|
||
"base_unit_price": 80.0,
|
||
})
|
||
assert resp.status_code == 404
|
||
|
||
def test_delete_pricing_rule(self, client, admin_headers, make_product):
|
||
"""PRICE-006: 删除定价规则(软删除)。"""
|
||
product = make_product()
|
||
create_resp = self._create_rule(client, admin_headers, product_id=product.id)
|
||
rule_id = create_resp.json()["data"]["id"]
|
||
resp = client.delete(f"/api/pricing-rules/{rule_id}", headers=admin_headers)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["data"]["deleted"] is True
|
||
# 删除后查询应返回 404
|
||
resp = client.get(f"/api/pricing-rules/{product.id}", headers=admin_headers)
|
||
assert resp.status_code == 404
|
||
|
||
def test_salesman_cannot_create_rule(self, client, salesman_headers, make_product):
|
||
"""业务员不能创建定价规则。"""
|
||
product = make_product()
|
||
resp = self._create_rule(client, salesman_headers, product_id=product.id)
|
||
assert resp.status_code == 403
|
||
|
||
def test_salesman_cannot_update_rule(self, client, salesman_headers, admin_headers, make_product):
|
||
"""业务员不能更新定价规则。"""
|
||
product = make_product()
|
||
create_resp = self._create_rule(client, admin_headers, product_id=product.id)
|
||
rule_id = create_resp.json()["data"]["id"]
|
||
resp = client.put(f"/api/pricing-rules/{rule_id}", headers=salesman_headers, json={
|
||
"base_unit_price": 80.0,
|
||
})
|
||
assert resp.status_code == 403
|
||
|
||
|
||
# ============================================================
|
||
# 报价计算 API 测试
|
||
# ============================================================
|
||
|
||
@pytest.mark.pricing
|
||
@pytest.mark.p1
|
||
class TestQuotationCalculation:
|
||
"""PRICE-010 ~ PRICE-020: 报价计算 API 测试。"""
|
||
|
||
def _setup_rule(self, client, headers, db_session, product, **overrides):
|
||
"""辅助: 在数据库中创建定价规则。"""
|
||
from backend.app.models.business import ProductPricingRule
|
||
defaults = {
|
||
"product_id": product.id,
|
||
"product_name": product.product_name,
|
||
"pricing_type": "area",
|
||
"base_unit_price": 50.0,
|
||
"pricing_unit": "㎡",
|
||
"pricing_inputs": json.dumps([
|
||
{"key": "length", "type": "number", "default_unit": "m"},
|
||
{"key": "width", "type": "number", "default_unit": "m"},
|
||
]),
|
||
"formula_expr": "$input.length_m * $input.width_m * $rule.base_unit_price",
|
||
"tax_rate": 0,
|
||
"tax_inclusive": 0,
|
||
"status": 1,
|
||
"deleted": 0,
|
||
}
|
||
defaults.update(overrides)
|
||
rule = ProductPricingRule(**defaults)
|
||
db_session.add(rule)
|
||
db_session.flush()
|
||
return rule
|
||
|
||
def test_calculate_area(self, client, admin_headers, db_session, make_product):
|
||
"""PRICE-010: 面积报价计算, 2m x 1.2m x ¥50 = ¥120。"""
|
||
product = make_product()
|
||
self._setup_rule(client, admin_headers, db_session, product, base_unit_price=50.0)
|
||
resp = client.post("/api/quotation/calculate", headers=admin_headers, json={
|
||
"product_id": product.id,
|
||
"user_inputs": {"length": 2, "width": 1.2},
|
||
})
|
||
assert resp.status_code == 200
|
||
data = resp.json()["data"]
|
||
assert data["base_cost"] == 120.0
|
||
assert data["cost_price"] == 120.0
|
||
|
||
def test_calculate_with_surcharge(self, client, admin_headers, db_session, make_product):
|
||
"""报价计算含附加费。"""
|
||
product = make_product()
|
||
self._setup_rule(client, admin_headers, db_session, product,
|
||
base_unit_price=50.0,
|
||
surcharge_json=json.dumps({
|
||
"urgent": {"name": "加急费", "method": "per_sqm", "price": 10}
|
||
}))
|
||
resp = client.post("/api/quotation/calculate", headers=admin_headers, json={
|
||
"product_id": product.id,
|
||
"user_inputs": {"length": 2, "width": 1.2},
|
||
"surcharge_selections": {"surcharge_urgent": True},
|
||
})
|
||
assert resp.status_code == 200
|
||
data = resp.json()["data"]
|
||
assert data["total_surcharge"] == 24.0
|
||
assert data["cost_price"] == 144.0
|
||
|
||
def test_calculate_with_tax_exclusive(self, client, admin_headers, db_session, make_product):
|
||
"""PRICE-017: 不含税报价计算, 100 + 13% = 113。"""
|
||
product = make_product()
|
||
self._setup_rule(client, admin_headers, db_session, product,
|
||
base_unit_price=50.0, tax_rate=13, tax_inclusive=0)
|
||
resp = client.post("/api/quotation/calculate", headers=admin_headers, json={
|
||
"product_id": product.id,
|
||
"user_inputs": {"length": 2, "width": 1},
|
||
})
|
||
data = resp.json()["data"]
|
||
assert data["tax_rate"] == 13
|
||
assert data["price_ex_tax"] == 100.0
|
||
assert data["tax_amount"] == 13.0
|
||
assert data["price_in_tax"] == 113.0
|
||
|
||
def test_calculate_with_tax_inclusive(self, client, admin_headers, db_session, make_product):
|
||
"""PRICE-016: 含税报价计算。"""
|
||
product = make_product()
|
||
self._setup_rule(client, admin_headers, db_session, product,
|
||
base_unit_price=56.5, tax_rate=13, tax_inclusive=1)
|
||
resp = client.post("/api/quotation/calculate", headers=admin_headers, json={
|
||
"product_id": product.id,
|
||
"user_inputs": {"length": 2, "width": 1},
|
||
})
|
||
data = resp.json()["data"]
|
||
assert data["price_in_tax"] == 113.0
|
||
assert data["price_ex_tax"] == 100.0
|
||
|
||
def test_calculate_no_rule_returns_404(self, client, admin_headers, make_product):
|
||
"""PRICE-020: 无定价规则的产品返回 404。"""
|
||
product = make_product()
|
||
resp = client.post("/api/quotation/calculate", headers=admin_headers, json={
|
||
"product_id": product.id,
|
||
"user_inputs": {"length": 2, "width": 1},
|
||
})
|
||
assert resp.status_code == 404
|
||
|
||
def test_calculate_with_price_tiers(self, client, admin_headers, db_session, make_product):
|
||
"""PRICE-018: 含价格层级的报价。"""
|
||
from backend.app.models.business import ProductPriceTier
|
||
product = make_product()
|
||
self._setup_rule(client, admin_headers, db_session, product, base_unit_price=50.0)
|
||
tier = ProductPriceTier(
|
||
product_id=product.id, tier_code="vip", tier_name="VIP",
|
||
price=45.0, price_unit="㎡", status=1, deleted=0,
|
||
)
|
||
db_session.add(tier)
|
||
db_session.flush()
|
||
resp = client.post("/api/quotation/calculate", headers=admin_headers, json={
|
||
"product_id": product.id,
|
||
"user_inputs": {"length": 2, "width": 1},
|
||
})
|
||
data = resp.json()["data"]
|
||
assert "vip" in data["sale_price_tier"]
|
||
assert data["sale_price_tier"]["vip"] == 45.0
|
||
|
||
def test_calculate_formula_detail(self, client, admin_headers, db_session, make_product):
|
||
"""报价结果包含公式说明。"""
|
||
product = make_product()
|
||
self._setup_rule(client, admin_headers, db_session, product, base_unit_price=50.0)
|
||
resp = client.post("/api/quotation/calculate", headers=admin_headers, json={
|
||
"product_id": product.id,
|
||
"user_inputs": {"length": 2, "width": 1.2},
|
||
})
|
||
data = resp.json()["data"]
|
||
assert "formula_detail" in data
|
||
assert "120.00" in data["formula_detail"]
|
||
|
||
def test_calculate_available_surcharge_options(self, client, admin_headers, db_session, make_product):
|
||
"""报价结果包含可用附加费选项。"""
|
||
product = make_product()
|
||
self._setup_rule(client, admin_headers, db_session, product,
|
||
base_unit_price=50.0,
|
||
surcharge_json=json.dumps({
|
||
"urgent": {"name": "加急费", "method": "per_sqm", "price": 10}
|
||
}))
|
||
resp = client.post("/api/quotation/calculate", headers=admin_headers, json={
|
||
"product_id": product.id,
|
||
"user_inputs": {"length": 2, "width": 1},
|
||
})
|
||
data = resp.json()["data"]
|
||
assert len(data["available_surcharge_options"]) == 1
|
||
assert data["available_surcharge_options"][0]["key"] == "urgent"
|
||
|
||
def test_salesman_can_calculate(self, client, salesman_headers, admin_headers, db_session, make_product):
|
||
"""业务员可以调用报价计算。"""
|
||
product = make_product()
|
||
self._setup_rule(client, admin_headers, db_session, product, base_unit_price=50.0)
|
||
resp = client.post("/api/quotation/calculate", headers=salesman_headers, json={
|
||
"product_id": product.id,
|
||
"user_inputs": {"length": 2, "width": 1},
|
||
})
|
||
assert resp.status_code == 200
|
||
|
||
|
||
# ============================================================
|
||
# 供应商成本 CRUD API 测试
|
||
# ============================================================
|
||
|
||
@pytest.mark.pricing
|
||
@pytest.mark.p1
|
||
class TestSupplierCostCRUD:
|
||
"""PRICE-030 ~ PRICE-033: 供应商成本 API 测试。"""
|
||
|
||
def _create_cost(self, client, headers, product_id, supplier_id, **overrides):
|
||
payload = {
|
||
"product_id": product_id,
|
||
"supplier_id": supplier_id,
|
||
"cost_price": 35.0,
|
||
"cost_unit": "㎡",
|
||
}
|
||
payload.update(overrides)
|
||
return client.post("/api/supplier-costs", headers=headers, json=payload)
|
||
|
||
def test_create_supplier_cost(self, client, admin_headers, make_product, make_supplier):
|
||
"""PRICE-030: 创建供应商成本。"""
|
||
product = make_product()
|
||
supplier = make_supplier()
|
||
resp = self._create_cost(client, admin_headers, product.id, supplier.id)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["data"]["cost_price"] == 35.0
|
||
|
||
def test_list_supplier_costs(self, client, admin_headers, make_product, make_supplier):
|
||
"""PRICE-031: 查询供应商成本列表。"""
|
||
product = make_product()
|
||
supplier = make_supplier()
|
||
self._create_cost(client, admin_headers, product.id, supplier.id)
|
||
resp = client.get(f"/api/supplier-costs?product_id={product.id}", headers=admin_headers)
|
||
assert resp.status_code == 200
|
||
assert len(resp.json()["data"]["list"]) >= 1
|
||
|
||
def test_update_supplier_cost(self, client, admin_headers, make_product, make_supplier):
|
||
"""PRICE-032: 更新供应商成本。"""
|
||
product = make_product()
|
||
supplier = make_supplier()
|
||
create_resp = self._create_cost(client, admin_headers, product.id, supplier.id)
|
||
cost_id = create_resp.json()["data"]["id"]
|
||
resp = client.put(f"/api/supplier-costs/{cost_id}", headers=admin_headers, json={
|
||
"cost_price": 40.0,
|
||
})
|
||
assert resp.status_code == 200
|
||
assert resp.json()["data"]["cost_price"] == 40.0
|
||
|
||
def test_delete_supplier_cost(self, client, admin_headers, make_product, make_supplier):
|
||
"""PRICE-033: 删除供应商成本(软删除)。"""
|
||
product = make_product()
|
||
supplier = make_supplier()
|
||
create_resp = self._create_cost(client, admin_headers, product.id, supplier.id)
|
||
cost_id = create_resp.json()["data"]["id"]
|
||
resp = client.delete(f"/api/supplier-costs/{cost_id}", headers=admin_headers)
|
||
assert resp.status_code == 200
|
||
# 删除后列表应为空
|
||
resp = client.get(f"/api/supplier-costs?product_id={product.id}", headers=admin_headers)
|
||
assert len(resp.json()["data"]["list"]) == 0
|
||
|
||
def test_salesman_cannot_create_cost(self, client, salesman_headers, make_product, make_supplier):
|
||
"""业务员不能创建供应商成本。"""
|
||
product = make_product()
|
||
supplier = make_supplier()
|
||
resp = self._create_cost(client, salesman_headers, product.id, supplier.id)
|
||
assert resp.status_code == 403
|
||
|
||
|
||
# ============================================================
|
||
# 价格层级 CRUD API 测试
|
||
# ============================================================
|
||
|
||
@pytest.mark.pricing
|
||
@pytest.mark.p1
|
||
class TestPriceTierCRUD:
|
||
"""PRICE-040 ~ PRICE-043: 价格层级 API 测试。"""
|
||
|
||
def _create_tier(self, client, headers, product_id, **overrides):
|
||
payload = {
|
||
"product_id": product_id,
|
||
"tier_code": "vip",
|
||
"tier_name": "VIP客户",
|
||
"price": 45.0,
|
||
"price_unit": "㎡",
|
||
}
|
||
payload.update(overrides)
|
||
return client.post("/api/price-tiers", headers=headers, json=payload)
|
||
|
||
def test_create_price_tier(self, client, admin_headers, make_product):
|
||
"""PRICE-040: 创建价格层级。"""
|
||
product = make_product()
|
||
resp = self._create_tier(client, admin_headers, product.id)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["data"]["tier_code"] == "vip"
|
||
assert resp.json()["data"]["price"] == 45.0
|
||
|
||
def test_list_price_tiers(self, client, admin_headers, make_product):
|
||
"""PRICE-041: 查询价格层级列表。"""
|
||
product = make_product()
|
||
self._create_tier(client, admin_headers, product.id)
|
||
resp = client.get(f"/api/price-tiers?product_id={product.id}", headers=admin_headers)
|
||
assert resp.status_code == 200
|
||
assert len(resp.json()["data"]["list"]) >= 1
|
||
|
||
def test_update_price_tier(self, client, admin_headers, make_product):
|
||
"""PRICE-042: 更新价格层级。"""
|
||
product = make_product()
|
||
create_resp = self._create_tier(client, admin_headers, product.id)
|
||
tier_id = create_resp.json()["data"]["id"]
|
||
resp = client.put(f"/api/price-tiers/{tier_id}", headers=admin_headers, json={
|
||
"price": 42.0,
|
||
})
|
||
assert resp.status_code == 200
|
||
assert resp.json()["data"]["price"] == 42.0
|
||
|
||
def test_delete_price_tier(self, client, admin_headers, make_product):
|
||
"""PRICE-043: 删除价格层级(软删除)。"""
|
||
product = make_product()
|
||
create_resp = self._create_tier(client, admin_headers, product.id)
|
||
tier_id = create_resp.json()["data"]["id"]
|
||
resp = client.delete(f"/api/price-tiers/{tier_id}", headers=admin_headers)
|
||
assert resp.status_code == 200
|
||
# 删除后列表应为空
|
||
resp = client.get(f"/api/price-tiers?product_id={product.id}", headers=admin_headers)
|
||
assert len(resp.json()["data"]["list"]) == 0
|
||
|
||
def test_salesman_can_list_tiers(self, client, salesman_headers, admin_headers, make_product):
|
||
"""业务员可以查看价格层级。"""
|
||
product = make_product()
|
||
self._create_tier(client, admin_headers, product.id)
|
||
resp = client.get(f"/api/price-tiers?product_id={product.id}", headers=salesman_headers)
|
||
assert resp.status_code == 200
|
||
|
||
def test_salesman_cannot_create_tier(self, client, salesman_headers, make_product):
|
||
"""业务员不能创建价格层级。"""
|
||
product = make_product()
|
||
resp = self._create_tier(client, salesman_headers, product.id)
|
||
assert resp.status_code == 403
|