dingdanquanliucheng/backend/tests/test_order_flow_nodes.py
2026-07-08 09:04:37 +08:00

544 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""订单流转节点逐点验证测试。
逐一测试订单生命周期中的每个状态转换节点,
确保每个节点都能正常工作。
流程图(有司机):
创建 -> pending_approve -> approved -> pending_driver -> accepted
-> picked_up -> pending_logistics -> in_transit -> shipped -> completed -> settled
流程图(无司机):
创建 -> pending_approve -> approved -> pending_logistics -> (快递100)
-> in_transit -> shipped -> completed -> settled
取消流:
pending_approve -> canceled (直接取消)
approved -> cancel_pending -> canceled (审批取消)
pending_driver -> cancel_fulfillment_pending -> canceled (履约取消)
pending_driver -> cancel_fulfillment_pending -> approved (拒绝取消恢复)
"""
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 -> pending_logistics (审批通过,无司机)"""
def test_approve_passes_order(self, client, salesman_headers, manager_headers, make_product):
"""审批通过后(无司机)状态应为 pending_logistics。"""
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"] == "pending_logistics"
@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_ApproveWithoutDriver:
"""节点3: approved -> pending_logistics (无司机审批)"""
def test_approve_without_driver_goes_to_pending_logistics(self, client, salesman_headers, manager_headers, make_product):
"""审批通过但不分配司机时,状态应为 pending_logistics。"""
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.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
"approve_result": "pass",
# 不提供 driver_id
})
assert resp.status_code == 200
assert resp.json()["data"]["order_status"] == "pending_logistics"
@pytest.mark.order
@pytest.mark.p0
class TestOrderFlowNode3b_SupplierText:
"""节点3b: approved -> pending_logistics (确认供应商文本,仅当有司机时)"""
def test_confirm_supplier_text_with_driver(self, client, salesman_headers, manager_headers, make_product, make_supplier):
"""有司机审批通过后,确认供应商文本后状态应为 pending_logistics。"""
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"]
# 审批通过(无司机,直接进入 pending_logistics
client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={
"approve_result": "pass",
})
# 确认供应商文本(此时订单已在 pending_logistics 状态)
resp = client.post(f"/api/orders/{order_id}/supplier-text/confirm", headers=manager_headers, json={
"supplier_id": supplier.id,
"text_content": "请安排生产10张产品",
})
# 由于订单已在 pending_logistics 状态,确认供应商文本应失败
assert resp.status_code == 400
@pytest.mark.order
@pytest.mark.p0
class TestOrderFlowNode4_ManualStatusChanges:
"""节点4-7: approved -> pending_driver -> accepted -> picked_up -> pending_logistics -> completed -> settled"""
def test_full_fulfillment_pipeline(self, client, manager_headers, make_order):
"""完整履约流程: approved -> pending_driver -> accepted -> picked_up -> pending_logistics -> completed -> settled。
注意: shipped 状态可由快递100自动判断不通过手动状态变更。
"""
order = make_order(order_status="approved")
order_id = order.id
transitions = [
("pending_driver", "分配司机"),
("accepted", "司机接单"),
("picked_up", "司机提货"),
("pending_logistics", "绑定物流单号"),
# shipped 状态由快递100自动判断不在此测试
("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.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_driver -> cancel_fulfillment_pending -> canceled (履约取消)"""
def test_fulfillment_cancel_flow(self, client, salesman_headers, manager_headers, make_order):
"""履约阶段取消需要经理批准。"""
order = make_order(order_status="pending_driver", 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_driver", 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_driver"
@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_unsupported_status(self, client, manager_headers, make_order):
"""approved 不能直接跳到未知状态。"""
order = make_order(order_status="approved")
resp = client.post(f"/api/orders/{order.id}/status", headers=manager_headers, json={
"target_status": "archived",
})
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="approved", 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="approved")
resp = client.post(f"/api/orders/{order.id}/status", headers=manager_headers, json={
"target_status": "pending_driver",
})
assert resp.status_code == 200