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

537 lines
21 KiB
Python

"""订单流转节点逐点验证测试。
逐一测试订单生命周期中的每个状态转换节点,
确保每个节点都能正常工作。
流程图:
创建 -> pending_approve -> approved -> pending_factory -> pending_driver
-> accepted -> picked_up -> delivered -> completed -> settled
取消流:
pending_approve -> canceled (直接取消)
approved -> cancel_pending -> canceled (审批取消)
pending_factory -> cancel_fulfillment_pending -> canceled (履约取消)
pending_factory -> cancel_fulfillment_pending -> pending_factory (拒绝取消恢复)
"""
from __future__ import annotations
import pytest
@pytest.mark.order
@pytest.mark.p0
class TestOrderFlowNode1_Create:
"""节点1: 创建订单 -> pending_approve"""
def test_create_order_goes_to_pending_approve(self, client, salesman_headers, make_product):
"""创建订单后状态应为 pending_approve。"""
product = make_product()
resp = client.post("/api/orders", headers=salesman_headers, json={
"customer_name": "节点测试客户",
"customer_mobile": "13800001001",
"items": [{
"product_id": product.id,
"product_name": product.product_name,
"specification": product.specification,
"unit": product.unit,
"quantity": 10,
"sale_price": 100,
"cost_price": 60,
}],
})
assert resp.status_code == 200
data = resp.json()["data"]
assert data["order_status"] == "pending_approve"
assert data["order_no"] is not None
return data["order_id"]
@pytest.mark.order
@pytest.mark.p0
class TestOrderFlowNode2_Approve:
"""节点2: pending_approve -> approved (审批通过)"""
def test_approve_passes_order(self, client, salesman_headers, manager_headers, make_product):
"""审批通过后状态应为 approved。"""
product = make_product()
# 创建
resp = client.post("/api/orders", headers=salesman_headers, json={
"customer_name": "审批测试客户",
"customer_mobile": "13800001002",
"items": [{
"product_id": product.id,
"product_name": product.product_name,
"specification": product.specification,
"unit": product.unit,
"quantity": 10,
"sale_price": 100,
"cost_price": 60,
}],
})
order_id = resp.json()["data"]["order_id"]
# 审批通过
resp = client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
"approve_result": "pass",
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "approved"
@pytest.mark.order
@pytest.mark.p0
class TestOrderFlowNode2b_Reject:
"""节点2b: pending_approve -> rejected (审批拒绝)"""
def test_reject_order(self, client, salesman_headers, manager_headers, make_product):
"""审批拒绝后状态应为 rejected。"""
product = make_product()
resp = client.post("/api/orders", headers=salesman_headers, json={
"customer_name": "拒绝测试客户",
"customer_mobile": "13800001003",
"items": [{
"product_id": product.id,
"product_name": product.product_name,
"specification": product.specification,
"unit": product.unit,
"quantity": 10,
"sale_price": 100,
"cost_price": 60,
}],
})
order_id = resp.json()["data"]["order_id"]
resp = client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
"approve_result": "reject",
"approve_opinion": "价格不合理",
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "rejected"
@pytest.mark.order
@pytest.mark.p0
class TestOrderFlowNode2c_RejectResubmit:
"""节点2c: rejected -> pending_approve (退回后重新提交)"""
def test_reject_then_resubmit(self, client, salesman_headers, manager_headers, make_product):
"""退回后重新提交应恢复为 pending_approve。"""
product = make_product()
resp = client.post("/api/orders", headers=salesman_headers, json={
"customer_name": "重提测试客户",
"customer_mobile": "13800001004",
"items": [{
"product_id": product.id,
"product_name": product.product_name,
"specification": product.specification,
"unit": product.unit,
"quantity": 10,
"sale_price": 100,
"cost_price": 60,
}],
})
order_id = resp.json()["data"]["order_id"]
# 拒绝
client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
"approve_result": "reject",
"approve_opinion": "需修改",
})
# 重新提交
resp = client.post(f"/api/orders/{order_id}/submit", headers=salesman_headers)
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "pending_approve"
@pytest.mark.order
@pytest.mark.p0
class TestOrderFlowNode3_SupplierText:
"""节点3: approved -> pending_factory (确认供应商文本)"""
def test_confirm_supplier_text_moves_to_pending_factory(self, client, salesman_headers, manager_headers, make_product, make_supplier):
"""确认供应商文本后状态应为 pending_factory。"""
product = make_product()
supplier = make_supplier()
resp = client.post("/api/orders", headers=salesman_headers, json={
"customer_name": "下发测试客户",
"customer_mobile": "13800001005",
"factory_id": supplier.id,
"items": [{
"product_id": product.id,
"product_name": product.product_name,
"specification": product.specification,
"unit": product.unit,
"quantity": 10,
"sale_price": 100,
"cost_price": 60,
}],
})
order_id = resp.json()["data"]["order_id"]
# 审批通过
client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
"approve_result": "pass",
})
# 确认供应商文本
resp = client.post(f"/api/orders/{order_id}/supplier-text/confirm", headers=manager_headers, json={
"supplier_id": supplier.id,
"text_content": "请安排生产10张产品",
})
assert resp.status_code == 200
# 检查订单状态
detail = client.get(f"/api/orders/{order_id}", headers=manager_headers)
assert detail.json()["data"]["order_status"] == "pending_factory"
@pytest.mark.order
@pytest.mark.p0
class TestOrderFlowNode4_ManualStatusChanges:
"""节点4-8: pending_factory -> pending_driver -> accepted -> picked_up -> delivered -> completed -> settled"""
def test_full_fulfillment_pipeline(self, client, manager_headers, make_order):
"""完整履约流程: pending_factory -> pending_driver -> accepted -> picked_up -> delivered -> completed -> settled。"""
order = make_order(order_status="pending_factory")
order_id = order.id
transitions = [
("pending_driver", "分配司机"),
("accepted", "司机接单"),
("picked_up", "司机提货"),
("delivered", "送达客户"),
("completed", "订单完成"),
("settled", "订单结算"),
]
for target_status, remark in transitions:
resp = client.post(f"/api/orders/{order_id}/status", headers=manager_headers, json={
"target_status": target_status,
"remark": remark,
})
assert resp.status_code == 200, f"转换到 {target_status} 失败: {resp.text}"
assert resp.json()["data"]["order_status"] == target_status, \
f"期望 {target_status}, 实际 {resp.json()['data']['order_status']}"
@pytest.mark.order
@pytest.mark.p0
class TestOrderFlowNode5_ProductionTrack:
"""节点5: pending_factory -> production -> shipped -> delivered"""
def test_production_track(self, client, manager_headers, make_order):
"""生产流程: pending_factory -> production -> shipped -> delivered。"""
order = make_order(order_status="pending_factory")
order_id = order.id
# 转到生产
resp = client.post(f"/api/orders/{order_id}/status", headers=manager_headers, json={
"target_status": "production",
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "production"
# 发货
resp = client.post(f"/api/orders/{order_id}/status", headers=manager_headers, json={
"target_status": "shipped",
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "shipped"
# 送达
resp = client.post(f"/api/orders/{order_id}/status", headers=manager_headers, json={
"target_status": "delivered",
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "delivered"
@pytest.mark.cancel
@pytest.mark.p0
class TestCancelNode1_DirectCancel:
"""取消节点1: pending_approve -> canceled (直接取消)"""
def test_direct_cancel_from_pending_approve(self, client, salesman_headers, make_order):
"""待审批订单可直接取消。"""
order = make_order(order_status="pending_approve", salesman_id=2)
resp = client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={
"cancel_reason": "客户取消",
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "canceled"
@pytest.mark.cancel
@pytest.mark.p0
class TestCancelNode2_ApprovedCancel:
"""取消节点2: approved -> cancel_pending -> canceled (审批取消)"""
def test_approved_cancel_flow(self, client, salesman_headers, manager_headers, make_order):
"""已审批订单取消需要经理批准。"""
order = make_order(order_status="approved", salesman_id=2)
# 申请取消
resp = client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={
"cancel_reason": "客户变更需求",
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "cancel_pending"
# 经理批准取消
resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=manager_headers, json={
"approve_result": "pass",
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "canceled"
def test_approved_cancel_refuse_restores(self, client, salesman_headers, manager_headers, make_order):
"""拒绝取消应恢复到原状态。"""
order = make_order(order_status="approved", salesman_id=2)
# 申请取消
client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={
"cancel_reason": "客户变更需求",
})
# 经理拒绝取消
resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=manager_headers, json={
"approve_result": "refuse",
"approve_opinion": "不同意取消",
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "approved"
@pytest.mark.cancel
@pytest.mark.p0
class TestCancelNode3_FulfillmentCancel:
"""取消节点3: pending_factory -> cancel_fulfillment_pending -> canceled (履约取消)"""
def test_fulfillment_cancel_flow(self, client, salesman_headers, manager_headers, make_order):
"""履约阶段取消需要经理批准。"""
order = make_order(order_status="pending_factory", salesman_id=2)
# 申请取消
resp = client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={
"cancel_reason": "客户取消订单",
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "cancel_fulfillment_pending"
# 经理批准
resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=manager_headers, json={
"approve_result": "pass",
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "canceled"
def test_fulfillment_cancel_refuse_restores(self, client, salesman_headers, manager_headers, make_order):
"""拒绝履约取消应恢复到原状态。"""
order = make_order(order_status="pending_factory", salesman_id=2)
# 申请取消
client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={
"cancel_reason": "客户取消订单",
})
# 经理拒绝
resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=manager_headers, json={
"approve_result": "refuse",
"approve_opinion": "已安排生产",
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "pending_factory"
@pytest.mark.order
@pytest.mark.p0
class TestInvalidTransitions:
"""非法状态转换应被拒绝。"""
def test_cannot_approve_draft(self, client, manager_headers, make_order):
"""草稿订单不能审批。"""
order = make_order(order_status="draft")
resp = client.post(f"/api/orders/{order.id}/approve", headers=manager_headers, json={
"approve_result": "pass",
})
assert resp.status_code in (400, 409)
def test_cannot_approve_canceled(self, client, manager_headers, make_order):
"""已取消订单不能审批。"""
order = make_order(order_status="canceled")
resp = client.post(f"/api/orders/{order.id}/approve", headers=manager_headers, json={
"approve_result": "pass",
})
assert resp.status_code in (400, 409)
def test_cannot_submit_approved(self, client, salesman_headers, make_order):
"""已审批订单不能再次提交。"""
order = make_order(order_status="approved", salesman_id=2)
resp = client.post(f"/api/orders/{order.id}/submit", headers=salesman_headers)
assert resp.status_code in (400, 409)
def test_cannot_skip_to_delivered(self, client, manager_headers, make_order):
"""pending_factory 不能直接跳到 delivered。"""
order = make_order(order_status="pending_factory")
resp = client.post(f"/api/orders/{order.id}/status", headers=manager_headers, json={
"target_status": "delivered",
})
assert resp.status_code in (400, 409)
def test_cannot_cancel_settled(self, client, salesman_headers, make_order):
"""已结算订单不能取消。"""
order = make_order(order_status="settled", salesman_id=2)
resp = client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={
"cancel_reason": "测试",
})
assert resp.status_code in (400, 409)
@pytest.mark.order
@pytest.mark.p0
class TestOrderEditNodes:
"""编辑节点测试。"""
def test_edit_pending_approve_order(self, client, salesman_headers, make_product):
"""pending_approve 状态可编辑。"""
product = make_product()
resp = client.post("/api/orders", headers=salesman_headers, json={
"customer_name": "编辑测试客户",
"customer_mobile": "13800001010",
"items": [{
"product_id": product.id,
"product_name": product.product_name,
"specification": product.specification,
"unit": product.unit,
"quantity": 10,
"sale_price": 100,
"cost_price": 60,
}],
})
order_id = resp.json()["data"]["order_id"]
# 编辑
resp = client.put(f"/api/orders/{order_id}", headers=salesman_headers, json={
"customer_name": "编辑后的客户",
"customer_mobile": "13800001010",
"items": [{
"product_id": product.id,
"product_name": product.product_name,
"specification": product.specification,
"unit": product.unit,
"quantity": 20,
"sale_price": 100,
"cost_price": 60,
}],
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "pending_approve"
def test_edit_rejected_order(self, client, salesman_headers, manager_headers, make_product):
"""rejected 状态可编辑。"""
product = make_product()
resp = client.post("/api/orders", headers=salesman_headers, json={
"customer_name": "退回编辑客户",
"customer_mobile": "13800001011",
"items": [{
"product_id": product.id,
"product_name": product.product_name,
"specification": product.specification,
"unit": product.unit,
"quantity": 10,
"sale_price": 100,
"cost_price": 60,
}],
})
order_id = resp.json()["data"]["order_id"]
# 拒绝
client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
"approve_result": "reject",
"approve_opinion": "需修改",
})
# 编辑
resp = client.put(f"/api/orders/{order_id}", headers=salesman_headers, json={
"customer_name": "退回编辑后的客户",
"customer_mobile": "13800001011",
"items": [{
"product_id": product.id,
"product_name": product.product_name,
"specification": product.specification,
"unit": product.unit,
"quantity": 15,
"sale_price": 120,
"cost_price": 70,
}],
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "rejected"
@pytest.mark.order
@pytest.mark.p0
class TestOrderSupplierTextNode:
"""供应商文本节点测试。"""
def test_generate_supplier_text(self, client, manager_headers, make_order, make_supplier):
"""生成供应商文本(不改变状态)。"""
supplier = make_supplier()
order = make_order(order_status="approved")
order.factory_id = supplier.id
resp = client.post(f"/api/orders/{order.id}/supplier-text", headers=manager_headers, json={
"supplier_id": supplier.id,
})
assert resp.status_code == 200
# 生成文本不改变状态
detail = client.get(f"/api/orders/{order.id}", headers=manager_headers)
assert detail.json()["data"]["order_status"] == "approved"
@pytest.mark.order
@pytest.mark.p0
class TestOrderPermissionNodes:
"""权限节点测试。"""
def test_salesman_cannot_approve(self, client, salesman_headers, make_order):
"""业务员不能审批。"""
order = make_order(order_status="pending_approve", salesman_id=2)
resp = client.post(f"/api/orders/{order.id}/approve", headers=salesman_headers, json={
"approve_result": "pass",
})
assert resp.status_code == 403
def test_salesman_cannot_change_status(self, client, salesman_headers, make_order):
"""业务员不能手动变更状态。"""
order = make_order(order_status="pending_factory", salesman_id=2)
resp = client.post(f"/api/orders/{order.id}/status", headers=salesman_headers, json={
"target_status": "pending_driver",
})
assert resp.status_code == 403
def test_salesman_cannot_cancel_approve(self, client, salesman_headers, make_order):
"""业务员不能审批取消。"""
order = make_order(order_status="cancel_pending", salesman_id=2)
resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=salesman_headers, json={
"approve_result": "pass",
})
assert resp.status_code == 403
def test_manager_can_approve(self, client, manager_headers, make_order):
"""经理可以审批。"""
order = make_order(order_status="pending_approve")
resp = client.post(f"/api/orders/{order.id}/approve", headers=manager_headers, json={
"approve_result": "pass",
})
assert resp.status_code == 200
def test_manager_can_change_status(self, client, manager_headers, make_order):
"""经理可以变更状态。"""
order = make_order(order_status="pending_factory")
resp = client.post(f"/api/orders/{order.id}/status", headers=manager_headers, json={
"target_status": "pending_driver",
})
assert resp.status_code == 200